Last active
          October 27, 2025 18:39 
        
      - 
      
- 
        Save sunmeat/9fa7ca42f785a682a3a7da9093f2c15d to your computer and use it in GitHub Desktop. 
    useReducer - покроковий візард (багатоетапна форма)
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | App.jsx: | |
| import {useEffect, useReducer, useRef, useState} from 'react'; | |
| import './App.css'; | |
| // початковий стан | |
| const initialState = { | |
| step: 1, | |
| components: { | |
| core: true, | |
| bladeOfProduction: false, | |
| shadowOfLegacy: false, | |
| firstCommit: false, | |
| repoCollection: false, | |
| alexanderPath: false, | |
| stepAcademy: false, | |
| }, | |
| installPath: 'C:/Games/HeroesJSReactIII', | |
| }; | |
| // редюсер для керування станом | |
| function reducer(state, action) { | |
| switch (action.type) { | |
| case 'NEXT_STEP': | |
| return {...state, step: Math.min(state.step + 1, 5)}; | |
| case 'PREV_STEP': | |
| return {...state, step: Math.max(state.step - 1, 1)}; | |
| case 'TOGGLE_COMPONENT': // для чекбоксів із додатками гри | |
| return { | |
| ...state, | |
| components: {...state.components, [action.payload]: !state.components[action.payload]}, | |
| }; | |
| case 'SET_INSTALL_PATH': | |
| return {...state, installPath: action.payload}; | |
| default: | |
| return state; | |
| } | |
| } | |
| // компоненти для кожного кроку | |
| function Welcome({dispatch, onStartAudio}) { | |
| return ( | |
| <div className="step-container"> | |
| <h2>Вітаємо в інсталяторі Heroes of JavaScript and React III</h2> | |
| <p> | |
| Цей майстер допоможе вам встановити Heroes of JavaScript and React III. | |
| Натисніть «Далі», щоб продовжити. | |
| </p> | |
| <div className="button-group"> | |
| <button disabled className="btn btn-disabled"> | |
| Назад | |
| </button> | |
| <button | |
| onClick={() => { | |
| // onStartAudio(); // запускаємо музику при першому натисканні !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | |
| dispatch({type: 'NEXT_STEP'}); // переходимо до наступного кроку | |
| }} | |
| className="btn btn-primary" | |
| > | |
| Далі | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function SelectComponents({state, dispatch}) { | |
| return ( | |
| <div className="step-container"> | |
| <h2>Виберіть компоненти</h2> | |
| <p>Виберіть доповнення для встановлення:</p> | |
| <div className="checkbox-group"> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={state.components.core} | |
| disabled | |
| /> | |
| Основна гра (обов’язково) | |
| </label> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={state.components.bladeOfProduction} | |
| onChange={() => dispatch({type: 'TOGGLE_COMPONENT', payload: 'bladeOfProduction'})} | |
| /> | |
| Клинок Продакшену | |
| </label> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={state.components.shadowOfLegacy} | |
| onChange={() => dispatch({type: 'TOGGLE_COMPONENT', payload: 'shadowOfLegacy'})} | |
| /> | |
| Тінь Легасі-коду | |
| </label> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={state.components.firstCommit} | |
| onChange={() => dispatch({type: 'TOGGLE_COMPONENT', payload: 'firstCommit'})} | |
| /> | |
| Перший Коміт | |
| </label> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={state.components.repoCollection} | |
| onChange={() => dispatch({type: 'TOGGLE_COMPONENT', payload: 'repoCollection'})} | |
| /> | |
| Повне зібрання репозиторіїв із ДЗ | |
| </label> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={state.components.alexanderPath} | |
| onChange={() => dispatch({type: 'TOGGLE_COMPONENT', payload: 'alexanderPath'})} | |
| /> | |
| Слідами Олександра | |
| </label> | |
| <label> | |
| <input | |
| type="checkbox" | |
| checked={state.components.stepAcademy} | |
| onChange={() => dispatch({type: 'TOGGLE_COMPONENT', payload: 'stepAcademy'})} | |
| /> | |
| Академія Реакту | |
| </label> | |
| </div> | |
| <div className="button-group"> | |
| <button onClick={() => dispatch({type: 'PREV_STEP'})} className="btn btn-secondary"> | |
| Назад | |
| </button> | |
| <button onClick={() => dispatch({type: 'NEXT_STEP'})} className="btn btn-primary"> | |
| Далі | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function ChooseInstallPath({state, dispatch}) { | |
| return ( | |
| <div className="step-container"> | |
| <h2>Виберіть шлях встановлення</h2> | |
| <p>Вкажіть каталог для встановлення гри:</p> | |
| <input | |
| type="text" | |
| value={state.installPath} | |
| onChange={(e) => dispatch({type: 'SET_INSTALL_PATH', payload: e.target.value})} | |
| className="input-path" | |
| /> | |
| <div className="button-group"> | |
| <button onClick={() => dispatch({type: 'PREV_STEP'})} className="btn btn-secondary"> | |
| Назад | |
| </button> | |
| <button onClick={() => dispatch({type: 'NEXT_STEP'})} className="btn btn-primary"> | |
| Далі | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function InstallProgress({dispatch}) { | |
| useEffect(() => { | |
| const timer = setTimeout(() => { | |
| dispatch({type: 'NEXT_STEP'}); | |
| }, 5000); // автоперехід через 5 секунд | |
| return () => clearTimeout(timer); | |
| }, [dispatch]); | |
| return ( | |
| <div className="step-container"> | |
| <h2>Встановлення...</h2> | |
| <p>Триває процес встановлення. Будь ласка, зачекайте...</p> | |
| <div className="progress-bar"> | |
| <div className="progress-bar-fill"></div> | |
| </div> | |
| <div className="button-group"> | |
| <button onClick={() => dispatch({type: 'PREV_STEP'})} className="btn btn-secondary"> | |
| Назад | |
| </button> | |
| <button disabled className="btn btn-disabled"> | |
| Далі | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function Finish({dispatch}) { | |
| return ( | |
| <div className="step-container"> | |
| <h2>Встановлення завершено</h2> | |
| <p> | |
| Гру Heroes of JavaScript and React III успішно встановлено! | |
| Натисніть «Готово», щоб вийти. | |
| </p> | |
| <div className="button-group"> | |
| <button onClick={() => dispatch({type: 'PREV_STEP'})} className="btn btn-secondary"> | |
| Назад | |
| </button> | |
| <button onClick={() => alert('Вітаємо!')} className="btn btn-primary"> | |
| Готово | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function App() { | |
| const [state, dispatch] = useReducer(reducer, initialState); | |
| const audioRef = useRef(null); | |
| const [isAudioStarted, setIsAudioStarted] = useState(false); | |
| // запуск музики | |
| const startAudio = () => { | |
| if (audioRef.current && !isAudioStarted) { | |
| audioRef.current.play().then(() => { | |
| setIsAudioStarted(true); | |
| }).catch((error) => { | |
| console.error('Музики не буде:', error); | |
| }); | |
| } | |
| }; | |
| // рендеринг поточного екрана інсталятора | |
| const renderStep = () => { | |
| switch (state.step) { | |
| case 1: | |
| return <Welcome dispatch={dispatch} onStartAudio={startAudio}/>; | |
| case 2: | |
| return <SelectComponents state={state} dispatch={dispatch}/>; | |
| case 3: | |
| return <ChooseInstallPath state={state} dispatch={dispatch}/>; | |
| case 4: | |
| return <InstallProgress dispatch={dispatch}/>; | |
| case 5: | |
| return <Finish dispatch={dispatch}/>; | |
| default: | |
| return null; | |
| } | |
| }; | |
| return ( | |
| <div className="app-container"> | |
| <h1>Інсталятор Heroes of JavaScript and React III</h1> | |
| <audio | |
| ref={audioRef} | |
| src="https://github.com/sunmeat/storage/raw/refs/heads/main/music/mp3/heroes.mp3" | |
| loop | |
| className="audio-player" | |
| /> | |
| {renderStep()} | |
| </div> | |
| ); | |
| } | |
| export default App; | |
| ================================================================================================================= | |
| App.css: | |
| body { | |
| font-family: Arial, sans-serif; | |
| background-color: #1e1e2f; | |
| color: #f0f0f0; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| .app-container { | |
| max-width: 700px; | |
| margin: 40px auto; | |
| padding: 20px; | |
| background-color: #292942; | |
| border-radius: 8px; | |
| box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); | |
| } | |
| h1 { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| font-size: 28px; | |
| color: #ffd700; | |
| } | |
| h2 { | |
| color: #ffffff; | |
| font-size: 22px; | |
| margin-bottom: 10px; | |
| } | |
| .step-container { | |
| margin-top: 20px; | |
| } | |
| p { | |
| font-size: 16px; | |
| margin-bottom: 20px; | |
| } | |
| .input-path { | |
| width: 97%; | |
| padding: 10px; | |
| font-size: 16px; | |
| border: 2px solid #444; | |
| border-radius: 4px; | |
| background-color: #1a1a2f; | |
| color: #fff; | |
| margin-bottom: 20px; | |
| } | |
| .checkbox-group label { | |
| display: block; | |
| margin-bottom: 10px; | |
| font-size: 16px; | |
| } | |
| input[type="checkbox"] { | |
| margin-right: 8px; | |
| transform: scale(1.2); | |
| } | |
| .button-group { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 20px; | |
| } | |
| .btn { | |
| padding: 10px 20px; | |
| font-size: 16px; | |
| border-radius: 6px; | |
| border: none; | |
| cursor: pointer; | |
| transition: background-color 0.2s ease; | |
| } | |
| .btn-primary { | |
| background-color: #4caf50; | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background-color: #45a049; | |
| } | |
| .btn-secondary { | |
| background-color: #2196f3; | |
| color: white; | |
| } | |
| .btn-secondary:hover { | |
| background-color: #1976d2; | |
| } | |
| .btn-disabled { | |
| background-color: #888; | |
| color: #ccc; | |
| cursor: not-allowed; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| background-color: #333; | |
| border-radius: 4px; | |
| height: 20px; | |
| overflow: hidden; | |
| margin-top: 10px; | |
| } | |
| .progress-bar-fill { | |
| height: 100%; | |
| width: 100%; | |
| background: linear-gradient(90deg, #00ff99, #00ccff); | |
| animation: fill 5s linear forwards; | |
| } | |
| @keyframes fill { | |
| 0% { | |
| width: 0; | |
| } | |
| 100% { | |
| width: 100%; | |
| } | |
| } | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment