[TestPage] add two new components - Navigation and Summary
This commit is contained in:
parent
a52da232e4
commit
af2cb9536c
|
@ -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;
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -20,6 +20,7 @@ export const QUERY_GENERATE_TEST_SIMILAR_QUALIFICATIONS = gql`
|
|||
) {
|
||||
generateTest(limit: $limitTest, qualificationIDs: [$qualificationID]) {
|
||||
id
|
||||
from
|
||||
content
|
||||
image
|
||||
answerA
|
||||
|
|
|
@ -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);
|
||||
|
|
Reference in New Issue