Ok, projekt nabiera tempa i kształtu. Całościowe omówienie produktu końcowego będzie, jak będzie apka, natomiast na razie pokazujemy proces twórczy. Pokazywanie skończonej całości i omawianie nie zawsze jest ok.
Dobra, najpierw zobaczmy initialContext (czy raczej initialState):
const initialState = {
timedQuestions: false,
timedQuiz: false,
questionTime: 5,
quizTime: 30,
mode: 'menu',
name: '',
score: 0,
answers: [],
shuffleAnswers: true,
activeIndex: 0,
};
Dodam, że key timera to będzie teraz activeIndex. Obiecałem sobie, że wywalę każdy useState, bo nie chcemy tego w naszym projekcie, chyba że to state komponenta (no jakiś komponent, który coś zaciąga z API i pokazuje ładowanie, do takich rzeczy useState ujdzie).
Dobra, to zobaczmy różne case reducera:
case 'restart': {
return {
...state,
score: 0,
mode: 'menu',
answers: [],
activeIndex: 0
};
}
case 'addAnswer': {
return {
...state,
answers: [...state.answers, action.payload],
activeIndex: state.activeIndex + 1
};
}
case 'skipAnswer': {
return {
...state,
answers: [...state.answers, null],
activeIndex: state.activeIndex + 1
};
}
Chyba proste, mamy jeszcze toggleShuffleQuestions:
case 'toggleShuffleAnswers': {
return {
...state,
shuffleAnswers: !state.shuffleAnswers
};
}
Ok, zobaczmy App.js:
import './App.css';
import { QuizProvider } from './QuizContext';
import { Mainloop } from './Mainloop';
function App() {
return (
<QuizProvider>
<Mainloop/>
</QuizProvider>
)
}
export default App;
Mainloop jest potrzebny, bo App nie ma dostępu do QuizProvidera. Tylko jego dzieci, tak wygląda nasz projekt (teoretycznie można ten provider wrzucić tam, gdzie mamy React.StrictMode, ale nie jest to standardowe zachowanie).
Ok, zobaczmy mainloop:
import { useQuiz } from "./QuizContext";
import { Menu } from "./Menu";
import { Quiz } from "./Quiz";
import { Finish } from "./Finish";
function Mainloop(){
const {mode} = useQuiz();
return (
<div className="App">
{mode === 'menu' && <Menu /> }
{mode === 'quiz' && <Quiz /> }
{mode === 'finish' && <Finish />}
</div>
)
}
export {Mainloop};
Chyba proste. Zobaczmy menu:
import { useRef, useEffect} from 'react';
import { useQuizWithDispatch } from './QuizContext';
import { Form } from './Form';
function Menu(){
const [{name }, dispatch] = useQuizWithDispatch();
const btnRef = useRef(null);
useEffect(() => {
if(name === ""){
btnRef.current.setAttribute("disabled", "");
} else {
btnRef.current.removeAttribute("disabled");
}
}, [name]);
return (
<>
<p>Your name: {name}</p>
<Form/>
<input type="text" onChange={(e) => dispatch({type: "changeName", payload:e.target.value})} value={name}/>
<button ref={btnRef} onClick={() => dispatch({type: "changeMode", payload:"quiz"})}>Start Quiz</button>
</>
)
};
export {Menu};
Całkiem ładne, zobaczmy form:
function Form(){
return (
<>
<QuestionTiming/>
<QuizTiming/>
<ShuffleAnswersCheckbox/>
</>
)
};
export {Form};
Dodałem nowy checkbox RFC w tym samym pliku:
function ShuffleAnswersCheckbox(){
const [{shuffleAnswers}, dispatch] = useQuizWithDispatch();
return (
<div>
<input
type="checkbox"
id="shuffleAnswers"
checked={shuffleAnswers}
onChange={() => dispatch({type: 'toggleShuffleAnswers'})} />
<label htmlFor="shuffleAnswers">Shuffle Answers: {shuffleAnswers ? 'YES' : 'NO'}</label>
</div>
)
}
Ok, zobaczmy Quiz:
function Quiz(){
const {score, name, answers, timedQuiz, quizTime, activeIndex} = useQuiz();
const dispatch = useQuizDispatch();
const activeQuestionIndex = answers.length;
const quizIsComplete = activeQuestionIndex === QUESTIONS.length;
return (
<>
{timedQuiz && (quizIsComplete || <QuizTimer timeout={quizTime}/>)}
<p>Your name: {name}</p>
<p>Your score: {score}</p>
{quizIsComplete || <Question/> }
{quizIsComplete || <SimpleSummary idx={activeIndex} len={QUESTIONS.length}/>}
{quizIsComplete && <button onClick={() => dispatch({type: "changeMode", payload:"finish"})}>Finish</button>}
</>
)
};
Nieźle odchudzony. Zobaczmy question:
function Question(){
const {timedQuestions, questionTime, activeIndex} = useQuiz();
const dispatch = useQuizDispatch();
let timer = questionTime * 1000;
const answers = QUESTIONS[activeIndex].answers;
return (
<>
{timedQuestions && <Timer key={activeIndex} timeout={timer} onTimeout={() => dispatch({type: 'skipAnswer'})}/> }
<h2>{QUESTIONS[activeIndex].text}</h2>
<Answers answers={answers} />
<button onClick={() => dispatch({type: 'skipAnswer'})}>Skip Answer</button>
</>
)
};
export {Question};
Koniec z drillowaniem propsów i wynoszeniem stanu do góry. I tak jest bajzel nawet z useContext i reducerem, trzeba będzie albo ogarnąć reduxa, a jak i tam bajzel będzie, to trzeba się będzie przestawić na Angulara, bo szczerze, staram się pisać czytelny kod, ale i tak trochę bajzel wychodzi.
To znaczy – zrobimy jeszcze porządki, zrobimy też strukturę folderów, ale przyznajmy – nawet z contextem sprawy szybko robią się dziwne. No ale ok, w każdym razie dla timera key to active index.
Komponent Answers też ładnie wygląda:
import { useQuiz, useQuizDispatch } from './QuizContext';
import QUESTIONS from './questions.js';
function Answers(){
const {shuffleAnswers, activeIndex} = useQuiz();
const dispatch = useQuizDispatch();
const answers = QUESTIONS[activeIndex].answers;
const readyAnswers = [...Object.entries(answers)];
if(shuffleAnswers){
readyAnswers.sort(() => Math.random() - 0.5);
}
function handleClick(idx){
if(idx === QUESTIONS[activeIndex].correctIndex){
dispatch({type: 'updateScore', payload: 1});
}
dispatch({type: 'addAnswer', payload: QUESTIONS[activeIndex].answers[idx] });
}
return (
<ul>
{readyAnswers.map((item) => {
const [key,val] = item;
return <li key={val} data-idx={key} onClick={() => handleClick(key)}>{val}</li>
})}
</ul>
)
};
export {Answers};
Jakbyśmy się pogubili to podaję strukturę naszych pytań:
const QUESTIONS = [
{
id: 'q1',
text: 'What is 2 + 2 = ?',
answers: {
optionA: '2',
optionB:'cztery',
optionC: '6',
optionD: '8',
},
correctIndex: "optionB"
},
{
id: 'q2',
text: 'What is 2 x 2 = ?',
answers: {
optionA: '2',
optionB:'4',
optionC: '6',
optionD: '8',
},
correctIndex: "optionB"
},
{
id: 'q3',
text: 'What is 2 + 2 x 2 = ?',
answers: {
optionA: '2',
optionB:'cztery',
optionC: 'sześć',
optionD: '8',
},
correctIndex: "optionC"
},
];
export default QUESTIONS;
Ok, rozkminiamy jak to działa, ja tymczasem przygotowuję kolejną wersję. Do zobaczenia!