[TestPage] add two new components - Navigation and Summary

This commit is contained in:
Dawid Wysokiński 2021-03-28 18:28:53 +02:00
parent a52da232e4
commit af2cb9536c
6 changed files with 218 additions and 11 deletions

View File

@ -0,0 +1,98 @@
import { makeStyles, useTheme } from '@material-ui/core/styles';
import { Button, ButtonGroup, useMediaQuery } from '@material-ui/core';
import {
ArrowBack as ArrowBackIcon,
ArrowForward as ArrowForwardIcon,
Done as DoneIcon,
Refresh as RefreshIcon,
} from '@material-ui/icons';
export interface NavigationProps {
hasPreviousTab: boolean;
hasNextTab: boolean;
isLastQuestion: boolean;
onRequestPrevTab: () => void;
onRequestNextTab: () => void;
onFinish: () => void;
onReset: () => void;
reviewMode: boolean;
}
const Navigation = ({
hasPreviousTab,
hasNextTab,
isLastQuestion,
onRequestPrevTab,
onRequestNextTab,
onFinish,
reviewMode,
onReset,
}: NavigationProps) => {
const classes = useStyles();
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down('sm'));
return (
<div className={classes.buttonContainer}>
<div className={classes.navButtonGroup}>
<Button
variant="contained"
color="primary"
startIcon={<ArrowBackIcon />}
disabled={!hasPreviousTab}
onClick={onRequestPrevTab}
>
Wróć
</Button>
<Button
variant="contained"
color="primary"
endIcon={<ArrowForwardIcon />}
disabled={!hasNextTab}
onClick={onRequestNextTab}
>
Dalej
</Button>
</div>
<ButtonGroup color="primary" variant="contained" fullWidth={matches}>
{isLastQuestion && !reviewMode && (
<Button onClick={onFinish} startIcon={<DoneIcon />}>
Zakończ test
</Button>
)}
{isLastQuestion && reviewMode && (
<Button startIcon={<RefreshIcon />} onClick={onReset}>
Rozpocznij od nowa
</Button>
)}
</ButtonGroup>
</div>
);
};
const useStyles = makeStyles(theme => {
return {
navButtonGroup: {
display: 'flex',
'& > *:not(:last-child)': {
marginRight: theme.spacing(2),
},
[theme.breakpoints.down('sm')]: {
justifyContent: 'space-between',
},
},
buttonContainer: {
marginTop: theme.spacing(3),
display: 'flex',
justifyContent: 'space-between',
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
'& > *:not(:last-child)': {
marginBottom: theme.spacing(1),
},
},
},
};
});
export default Navigation;

View File

@ -16,12 +16,12 @@ export interface QuestionProps {
reviewMode: boolean;
}
function Question({
const Question = ({
question,
answer,
onChangeAnswer,
reviewMode,
}: QuestionProps) {
}: QuestionProps) => {
const classes = useStyles();
return (
<div className={classes.question}>
@ -109,7 +109,7 @@ function Question({
})}
</div>
);
}
};
const useStyles = makeStyles(theme => {
return {

View File

@ -0,0 +1,55 @@
import { useMemo } from 'react';
import { polishPlurals } from 'polish-plurals';
import { calculateDifferenceBetweenTwoDates } from 'libs/hooks';
import { Answer, Question } from 'libs/graphql';
import { Typography } from '@material-ui/core';
export interface SummaryProps {
answers: Answer[];
questions: Question[];
reviewMode: boolean;
startedAt: Date;
endedAt: Date;
}
const Summary = ({
answers,
questions,
reviewMode,
startedAt,
endedAt,
}: SummaryProps) => {
const correctAnswers = useMemo(() => {
if (!reviewMode) return 0;
return answers.filter(
(answer, index) => questions[index].correctAnswer === answer
).length;
}, [answers, questions, reviewMode]);
const { hours, seconds, minutes } = useMemo(() => {
return calculateDifferenceBetweenTwoDates(startedAt, endedAt);
}, [startedAt, endedAt]);
const total = questions.length;
return (
<div>
<Typography gutterBottom component="h3" variant="h4">
Czas rozwiązywania: {hours}{' '}
{polishPlurals('godzina', 'godziny', 'godzin', hours)}, {minutes}{' '}
{polishPlurals('minuta', 'minuty', 'minut', minutes)} i {seconds}{' '}
{polishPlurals('sekunda', 'sekundy', 'sekund', seconds)}.
</Typography>
<Typography component="h3" variant="h4">
Twój wynik:{' '}
<strong>{Math.ceil((correctAnswers / total) * 100)}%</strong>
</Typography>
<Typography>
Udzielono poprawnej odpowiedzi na <strong>{correctAnswers}</strong>{' '}
{polishPlurals('pytanie', 'pytania', 'pytań', correctAnswers)} z{' '}
<strong>{total}</strong>.
</Typography>
</div>
);
};
export default Summary;

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Answer, Qualification, Question as QuestionT } from 'libs/graphql';
import {
@ -13,6 +13,8 @@ import {
import Section from 'common/Section/Section';
import TabPanel from './TabPanel';
import Question from './Question';
import Navigation from './Navigation';
import Summary from './Summary';
export interface TestProps {
initialQuestions: QuestionT[];
@ -20,19 +22,47 @@ export interface TestProps {
}
const Test = ({ initialQuestions, qualification }: TestProps) => {
const headingRef = useRef<HTMLSpanElement | null>(null);
const [questions, setQuestions] = useState(initialQuestions);
const [selectedAnswers, setSelectedAnswers] = useState<Answer[]>(
new Array(initialQuestions.length).fill('')
);
const [currentTab, setCurrentTab] = useState(0);
const [reviewMode, setReviewMode] = useState(false);
const [startedAt, setStartedAt] = useState(new Date());
const [endedAt, setEndedAt] = useState(new Date());
useEffect(() => {
if (headingRef.current?.scrollIntoView) {
headingRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [currentTab]);
const handleReset = () => {
setStartedAt(new Date());
setEndedAt(new Date());
setSelectedAnswers(new Array(initialQuestions.length).fill(''));
setCurrentTab(0);
setReviewMode(false);
};
const handleFinish = () => {
setEndedAt(new Date());
setCurrentTab(currentTab => currentTab + 1);
setReviewMode(true);
};
return (
<Section>
<Container>
<Typography align="center" variant="h1" gutterBottom>
Kwalifikacja <strong>{qualification.code}</strong>
</Typography>
<header>
<Typography ref={headingRef} align="center" variant="h1" gutterBottom>
Kwalifikacja <strong>{qualification.code}</strong>
</Typography>
</header>
{questions.length === 0 ? (
<Typography align="center" variant="h2">
Do tej kwalifikacji nie zostały dodane żadne pytania.
@ -57,7 +87,7 @@ const Test = ({ initialQuestions, qualification }: TestProps) => {
</AppBar>
<Box padding={3}>
{questions.map((question, index) => (
<TabPanel key={question.id} value={index} index={index}>
<TabPanel key={question.id} value={currentTab} index={index}>
<Question
question={question}
answer={selectedAnswers[index]}
@ -72,6 +102,29 @@ const Test = ({ initialQuestions, qualification }: TestProps) => {
/>
</TabPanel>
))}
<TabPanel value={currentTab} index={questions.length}>
<Summary
answers={selectedAnswers}
questions={questions}
reviewMode={reviewMode}
startedAt={startedAt}
endedAt={endedAt}
/>
</TabPanel>
<Navigation
hasPreviousTab={currentTab !== 0}
hasNextTab={
currentTab + 1 !== questions.length + (reviewMode ? 1 : 0)
}
onRequestPrevTab={() => setCurrentTab(currentTab - 1)}
onRequestNextTab={() => setCurrentTab(currentTab + 1)}
isLastQuestion={
currentTab + 1 === questions.length + (reviewMode ? 1 : 0)
}
reviewMode={reviewMode}
onReset={handleReset}
onFinish={handleFinish}
/>
</Box>
</Paper>
)}

View File

@ -20,6 +20,7 @@ export const QUERY_GENERATE_TEST_SIMILAR_QUALIFICATIONS = gql`
) {
generateTest(limit: $limitTest, qualificationIDs: [$qualificationID]) {
id
from
content
image
answerA

View File

@ -7,7 +7,7 @@ export type DifferenceBetweenDates = {
seconds: number;
};
const calculateDifference = (
export const calculateDifferenceBetweenTwoDates = (
dateLeft: Date,
dateRight: Date
): DifferenceBetweenDates => {
@ -23,12 +23,12 @@ const calculateDifference = (
export const useCountdown = (dateRight: Date): DifferenceBetweenDates => {
const [difference, setDifference] = useState<DifferenceBetweenDates>(
calculateDifference(new Date(), dateRight)
calculateDifferenceBetweenTwoDates(new Date(), dateRight)
);
useEffect(() => {
const timeout = setTimeout(() => {
setDifference(calculateDifference(new Date(), dateRight));
setDifference(calculateDifferenceBetweenTwoDates(new Date(), dateRight));
}, 1000);
return () => {
clearTimeout(timeout);