Created
May 29, 2025 11:16
-
-
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 | |
/> | |
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