Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Created May 29, 2025 11:16
Show Gist options
  • Save sunmeat/9fa7ca42f785a682a3a7da9093f2c15d to your computer and use it in GitHub Desktop.
Save sunmeat/9fa7ca42f785a682a3a7da9093f2c15d to your computer and use it in GitHub Desktop.
useReducer - инсталлятор
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
/>
Core Game (Required)
</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'})}
/>
Тень Legacy-кода
</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">
Back
</button>
<button onClick={() => dispatch({type: 'NEXT_STEP'})} className="btn btn-primary">
Next
</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">
Back
</button>
<button onClick={() => dispatch({type: 'NEXT_STEP'})} className="btn btn-primary">
Next
</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">
Back
</button>
<button disabled className="btn btn-disabled">
Next
</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:
/* 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