mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
feat(client): quiz challenge with validation (#56163)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -162,6 +162,7 @@ shared/utils/get-lines.test.js
|
||||
shared/utils/validate.js
|
||||
shared/utils/validate.test.js
|
||||
shared/utils/is-audited.js
|
||||
shared/utils/shuffle-array.js
|
||||
|
||||
### Old Generated files ###
|
||||
# These files are no longer generated by the client, but can
|
||||
|
||||
@@ -483,6 +483,12 @@
|
||||
"preview-external-window": "Preview currently showing in external window.",
|
||||
"fill-in-the-blank": "Fill in the blank",
|
||||
"blank": "blank",
|
||||
"quiz": {
|
||||
"correct-answer": "Correct",
|
||||
"incorrect-answer": "Incorrect",
|
||||
"unanswered-questions": "The following questions are unanswered: {{ unansweredQuestions }}. You must answer all questions.",
|
||||
"have-n-correct-questions": "You have {{ correctAnswerCount }} out of {{ total }} questions correct."
|
||||
},
|
||||
"exam": {
|
||||
"qualified": "Congratulations, you have completed all the requirements to qualify for the exam.",
|
||||
"not-qualified": "You have not met the requirements to be eligible for the exam. To qualify, please complete the following challenges:",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@freecodecamp/loop-protect": "3.0.0",
|
||||
"@freecodecamp/react-calendar-heatmap": "1.1.0",
|
||||
"@freecodecamp/ui": "1.2.0",
|
||||
"@freecodecamp/ui": "2.1.0",
|
||||
"@growthbook/growthbook-react": "0.20.0",
|
||||
"@loadable/component": "5.16.3",
|
||||
"@reach/router": "1.3.4",
|
||||
|
||||
10
client/src/templates/Challenges/quiz/show.css
Normal file
10
client/src/templates/Challenges/quiz/show.css
Normal file
@@ -0,0 +1,10 @@
|
||||
/* Override global ul styles */
|
||||
.quiz-challenge-container ul {
|
||||
padding-inline-start: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
/* Override the bottom margin set in global.css */
|
||||
.quiz-challenge-container .quiz-answer-label p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
// Package Utilities
|
||||
import { graphql } from 'gatsby';
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { ObserveKeys } from 'react-hotkeys';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Container, Col, Row, Button, Quiz } from '@freecodecamp/ui';
|
||||
import { Container, Col, Row, Button, Quiz, useQuiz } from '@freecodecamp/ui';
|
||||
|
||||
// Local Utilities
|
||||
import { shuffleArray } from '../../../../../shared/utils/shuffle-array';
|
||||
import Spacer from '../../../components/helpers/spacer';
|
||||
import LearnLayout from '../../../components/layouts/learn';
|
||||
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types';
|
||||
@@ -28,6 +27,9 @@ import {
|
||||
initTests
|
||||
} from '../redux/actions';
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
import PrismFormatted from '../components/prism-formatted';
|
||||
|
||||
import './show.css';
|
||||
|
||||
// Redux Setup
|
||||
const mapStateToProps = createSelector(
|
||||
@@ -43,8 +45,7 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
updateChallengeMeta,
|
||||
challengeMounted,
|
||||
updateSolutionFormValues,
|
||||
openCompletionModal: () => openModal('completion'),
|
||||
openHelpModal: () => openModal('help')
|
||||
openCompletionModal: () => openModal('completion')
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
@@ -57,52 +58,102 @@ interface ShowQuizProps {
|
||||
initTests: (xs: Test[]) => void;
|
||||
isChallengeCompleted: boolean;
|
||||
openCompletionModal: () => void;
|
||||
openHelpModal: () => void;
|
||||
pageContext: {
|
||||
challengeMeta: ChallengeMeta;
|
||||
};
|
||||
t: TFunction;
|
||||
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
||||
updateSolutionFormValues: () => void;
|
||||
}
|
||||
|
||||
interface ShowQuizState {
|
||||
hasSubmitted: boolean;
|
||||
quiz: null;
|
||||
}
|
||||
const ShowQuiz = ({
|
||||
challengeMounted,
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
fields: { tests },
|
||||
title,
|
||||
description,
|
||||
challengeType,
|
||||
helpCategory,
|
||||
superBlock,
|
||||
block,
|
||||
translationPending,
|
||||
quizzes
|
||||
}
|
||||
}
|
||||
},
|
||||
pageContext: { challengeMeta },
|
||||
initTests,
|
||||
updateChallengeMeta,
|
||||
openCompletionModal,
|
||||
isChallengeCompleted
|
||||
}: ShowQuizProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { nextChallengePath, prevChallengePath } = challengeMeta;
|
||||
const container = useRef<HTMLElement | null>(null);
|
||||
|
||||
// Component
|
||||
class ShowQuiz extends Component<ShowQuizProps, ShowQuizState> {
|
||||
static displayName: string;
|
||||
private container: React.RefObject<HTMLElement> = React.createRef();
|
||||
// Campers are not allowed to change their answers once the quiz is submitted.
|
||||
// `hasSubmitted` is used as a flag to disable the quiz.
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||
|
||||
constructor(props: ShowQuizProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasSubmitted: false,
|
||||
quiz: null
|
||||
};
|
||||
// `isPassed` is used as a flag to conditionally render the test or submit button.
|
||||
const [isPassed, setIsPassed] = useState(false);
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
const blockNameTitle = `${t(
|
||||
`intro:${superBlock}.blocks.${block}.title`
|
||||
)} - ${title}`;
|
||||
|
||||
componentDidMount(): void {
|
||||
const {
|
||||
challengeMounted,
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
fields: { tests },
|
||||
title,
|
||||
challengeType,
|
||||
helpCategory
|
||||
}
|
||||
}
|
||||
},
|
||||
pageContext: { challengeMeta },
|
||||
initTests,
|
||||
updateChallengeMeta
|
||||
} = this.props;
|
||||
const [quizId] = useState(Math.floor(Math.random() * quizzes.length));
|
||||
const quiz = quizzes[quizId].questions;
|
||||
|
||||
// Initialize the data passed to `useQuiz`
|
||||
const [initialQuizData] = useState(
|
||||
quiz.map(question => {
|
||||
const distractors = question.distractors.map((distractor, index) => {
|
||||
return {
|
||||
label: (
|
||||
<PrismFormatted className='quiz-answer-label' text={distractor} />
|
||||
),
|
||||
value: index + 1
|
||||
};
|
||||
});
|
||||
|
||||
const answer = {
|
||||
label: (
|
||||
<PrismFormatted
|
||||
className='quiz-answer-label'
|
||||
text={question.answer}
|
||||
/>
|
||||
),
|
||||
value: 4
|
||||
};
|
||||
|
||||
return {
|
||||
question: <PrismFormatted text={question.text} />,
|
||||
answers: shuffleArray([...distractors, answer]),
|
||||
correctAnswer: answer.value
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
questions: quizData,
|
||||
validateAnswers,
|
||||
correctAnswerCount
|
||||
} = useQuiz({
|
||||
initialQuestions: initialQuizData,
|
||||
validationMessages: {
|
||||
correct: t('learn.quiz.correct-answer'),
|
||||
incorrect: t('learn.quiz.incorrect-answer')
|
||||
},
|
||||
onSuccess: () => {
|
||||
openCompletionModal();
|
||||
setIsPassed(true);
|
||||
},
|
||||
onFailure: () => setIsPassed(false)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initTests(tests);
|
||||
updateChallengeMeta({
|
||||
...challengeMeta,
|
||||
@@ -111,147 +162,130 @@ class ShowQuiz extends Component<ShowQuizProps, ShowQuizState> {
|
||||
helpCategory
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
this.container.current?.focus();
|
||||
}
|
||||
container.current?.focus();
|
||||
// This effect should be run once on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
componentDidUpdate(prevProps: ShowQuizProps): void {
|
||||
const {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: { title: prevTitle }
|
||||
}
|
||||
}
|
||||
} = prevProps;
|
||||
const {
|
||||
challengeMounted,
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: { title: currentTitle, challengeType, helpCategory }
|
||||
}
|
||||
},
|
||||
pageContext: { challengeMeta },
|
||||
updateChallengeMeta
|
||||
} = this.props;
|
||||
if (prevTitle !== currentTitle) {
|
||||
updateChallengeMeta({
|
||||
...challengeMeta,
|
||||
title: currentTitle,
|
||||
challengeType,
|
||||
helpCategory
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
console.log('handleSubmit');
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
title,
|
||||
// challengeType,
|
||||
description,
|
||||
superBlock,
|
||||
block,
|
||||
translationPending,
|
||||
quizzes
|
||||
}
|
||||
}
|
||||
},
|
||||
// openCompletionModal,
|
||||
openHelpModal,
|
||||
pageContext: {
|
||||
challengeMeta: { nextChallengePath, prevChallengePath }
|
||||
},
|
||||
t,
|
||||
isChallengeCompleted
|
||||
} = this.props;
|
||||
|
||||
const blockNameTitle = `${t(
|
||||
`intro:${superBlock}.blocks.${block}.title`
|
||||
)} - ${title}`;
|
||||
|
||||
const random = Math.floor(Math.random() * quizzes.length);
|
||||
const quiz = quizzes[random].questions;
|
||||
const quizForComponent = quiz.map(question => {
|
||||
const distractors = question.distractors.map((distractor, index) => {
|
||||
return {
|
||||
label: distractor,
|
||||
value: index + 1
|
||||
};
|
||||
});
|
||||
const answer = {
|
||||
label: question.answer,
|
||||
value: 4
|
||||
};
|
||||
|
||||
return {
|
||||
question: question.text,
|
||||
answers: [...distractors, answer]
|
||||
};
|
||||
useEffect(() => {
|
||||
updateChallengeMeta({
|
||||
...challengeMeta,
|
||||
title,
|
||||
challengeType,
|
||||
helpCategory
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
}, [
|
||||
title,
|
||||
challengeMeta,
|
||||
challengeType,
|
||||
helpCategory,
|
||||
challengeMounted,
|
||||
updateChallengeMeta
|
||||
]);
|
||||
|
||||
return (
|
||||
<Hotkeys
|
||||
executeChallenge={() => {
|
||||
this.handleSubmit();
|
||||
}}
|
||||
containerRef={this.container}
|
||||
nextChallengePath={nextChallengePath}
|
||||
prevChallengePath={prevChallengePath}
|
||||
>
|
||||
<LearnLayout>
|
||||
<Helmet
|
||||
title={`${blockNameTitle} | ${t('learn.learn')} | freeCodeCamp.org`}
|
||||
/>
|
||||
<Container>
|
||||
<Row>
|
||||
const handleAnswersCheck = () => {
|
||||
validateAnswers();
|
||||
setHasSubmitted(true);
|
||||
};
|
||||
|
||||
const handleSubmitAndGo = () => {
|
||||
openCompletionModal();
|
||||
};
|
||||
|
||||
function getErrorMessage() {
|
||||
if (!hasSubmitted) return '';
|
||||
|
||||
const unansweredList = quizData.reduce<number[]>(
|
||||
(acc, curr, id) => (curr.selectedAnswer == null ? [...acc, id + 1] : acc),
|
||||
[]
|
||||
);
|
||||
|
||||
if (unansweredList.length > 0) {
|
||||
return t('learn.quiz.unanswered-questions', {
|
||||
unansweredQuestions: unansweredList.join(', ')
|
||||
});
|
||||
}
|
||||
|
||||
return t('learn.quiz.have-n-correct-questions', {
|
||||
correctAnswerCount,
|
||||
total: quiz.length
|
||||
});
|
||||
}
|
||||
|
||||
const errorMessage = getErrorMessage();
|
||||
|
||||
return (
|
||||
<Hotkeys
|
||||
executeChallenge={!isPassed ? handleAnswersCheck : handleSubmitAndGo}
|
||||
containerRef={container}
|
||||
nextChallengePath={nextChallengePath}
|
||||
prevChallengePath={prevChallengePath}
|
||||
>
|
||||
<LearnLayout>
|
||||
<Helmet
|
||||
title={`${blockNameTitle} | ${t('learn.learn')} | freeCodeCamp.org`}
|
||||
/>
|
||||
<Container className='quiz-challenge-container'>
|
||||
<Row>
|
||||
<Spacer size='medium' />
|
||||
<ChallengeTitle
|
||||
isCompleted={isChallengeCompleted}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
{title}
|
||||
</ChallengeTitle>
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<ChallengeDescription description={description} />
|
||||
<ObserveKeys>
|
||||
<Quiz questions={quizData} disabled={hasSubmitted} />
|
||||
</ObserveKeys>
|
||||
<Spacer size='medium' />
|
||||
<ChallengeTitle
|
||||
isCompleted={isChallengeCompleted}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
{title}
|
||||
</ChallengeTitle>
|
||||
<div aria-live='polite' aria-atomic='true'>
|
||||
{errorMessage}
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
{/*
|
||||
There are three cases for the button display:
|
||||
1. Campers submit the answers but don't pass
|
||||
2. Campers submit the answers and pass, click the submit button on the completion modal
|
||||
3. Campers submit the answers and pass, but they close the completion modal
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<ChallengeDescription description={description} />
|
||||
<ObserveKeys>
|
||||
<Quiz questions={quizForComponent} />
|
||||
</ObserveKeys>
|
||||
<Spacer size='medium' />
|
||||
This rendering logic is only handling (2) and (3).
|
||||
TODO: Update the logic to handle (1).
|
||||
The code should render a link that points campers to the module's review block.
|
||||
*/}
|
||||
{!isPassed ? (
|
||||
<Button
|
||||
block={true}
|
||||
variant='primary'
|
||||
onClick={() => this.handleSubmit()}
|
||||
onClick={handleAnswersCheck}
|
||||
>
|
||||
{t('buttons.check-answer')}
|
||||
</Button>
|
||||
<Spacer size='xxSmall' />
|
||||
<Button block={true} variant='primary' onClick={openHelpModal}>
|
||||
{t('buttons.ask-for-help')}
|
||||
) : (
|
||||
<Button
|
||||
block={true}
|
||||
variant='primary'
|
||||
onClick={handleSubmitAndGo}
|
||||
>
|
||||
{t('buttons.submit-and-go')}
|
||||
</Button>
|
||||
<Spacer size='large' />
|
||||
</Col>
|
||||
<CompletionModal />
|
||||
</Row>
|
||||
</Container>
|
||||
</LearnLayout>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
<Spacer size='large' />
|
||||
</Col>
|
||||
<CompletionModal />
|
||||
</Row>
|
||||
</Container>
|
||||
</LearnLayout>
|
||||
</Hotkeys>
|
||||
);
|
||||
};
|
||||
|
||||
ShowQuiz.displayName = 'ShowQuiz';
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withTranslation()(ShowQuiz));
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ShowQuiz);
|
||||
|
||||
export const query = graphql`
|
||||
query QuizChallenge($id: String!) {
|
||||
@@ -266,6 +300,10 @@ export const query = graphql`
|
||||
fields {
|
||||
blockName
|
||||
slug
|
||||
tests {
|
||||
text
|
||||
testString
|
||||
}
|
||||
}
|
||||
quizzes {
|
||||
questions {
|
||||
|
||||
65
pnpm-lock.yaml
generated
65
pnpm-lock.yaml
generated
@@ -468,8 +468,8 @@ importers:
|
||||
specifier: 1.1.0
|
||||
version: 1.1.0(react@16.14.0)
|
||||
'@freecodecamp/ui':
|
||||
specifier: 1.2.0
|
||||
version: 1.2.0(@types/react-dom@16.9.24)(@types/react@16.14.56)
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
|
||||
'@growthbook/growthbook-react':
|
||||
specifier: 0.20.0
|
||||
version: 0.20.0(react@16.14.0)
|
||||
@@ -1122,13 +1122,13 @@ importers:
|
||||
version: 4.17.12
|
||||
babel-loader:
|
||||
specifier: 8.3.0
|
||||
version: 8.3.0(@babel/core@7.23.7)(webpack@5.90.3(webpack-cli@4.10.0))
|
||||
version: 8.3.0(@babel/core@7.23.7)(webpack@5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3)))
|
||||
chai:
|
||||
specifier: 4.4.1
|
||||
version: 4.4.1
|
||||
copy-webpack-plugin:
|
||||
specifier: 9.1.0
|
||||
version: 9.1.0(webpack@5.90.3(webpack-cli@4.10.0))
|
||||
version: 9.1.0(webpack@5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3)))
|
||||
enzyme:
|
||||
specifier: 3.11.0
|
||||
version: 3.11.0
|
||||
@@ -1155,7 +1155,7 @@ importers:
|
||||
version: 0.12.5
|
||||
webpack:
|
||||
specifier: 5.90.3
|
||||
version: 5.90.3(webpack-cli@4.10.0)
|
||||
version: 5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))
|
||||
webpack-cli:
|
||||
specifier: 4.10.0
|
||||
version: 4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3)
|
||||
@@ -2934,9 +2934,12 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
'@freecodecamp/ui@1.2.0':
|
||||
resolution: {integrity: sha512-WXNMwT3UO5FfN4jb5JhCel6Ddrnr/KMG3yyKtFF33I7JuGb8P7ykRCuM4aeCpceH7L1aXXmEZ7gcf1Gjgy3WDw==}
|
||||
'@freecodecamp/ui@2.1.0':
|
||||
resolution: {integrity: sha512-KL9QVvBlVjkls9BWTpsxNQdp88OmR+x9qE/jNvKcGRByFVsLXbAblb1cRcz8YXPPdB4vpImMT8Q3baxZTHn28Q==}
|
||||
engines: {node: '>=20', pnpm: '9'}
|
||||
peerDependencies:
|
||||
react: ^16.14.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
'@gatsbyjs/reach-router@1.3.9':
|
||||
resolution: {integrity: sha512-/354IaUSM54xb7K/TxpLBJB94iEAJ3P82JD38T8bLnIDWF+uw8+W/82DKnQ7y24FJcKxtVmG43aiDLG88KSuYQ==}
|
||||
@@ -16718,7 +16721,7 @@ snapshots:
|
||||
prop-types: 15.8.1
|
||||
react: 16.14.0
|
||||
|
||||
'@freecodecamp/ui@1.2.0(@types/react-dom@16.9.24)(@types/react@16.14.56)':
|
||||
'@freecodecamp/ui@2.1.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)':
|
||||
dependencies:
|
||||
'@fortawesome/fontawesome-svg-core': 6.6.0
|
||||
'@fortawesome/free-solid-svg-icons': 6.6.0
|
||||
@@ -18023,7 +18026,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.8.0
|
||||
tapable: 2.2.1
|
||||
webpack: 5.90.3(webpack-cli@4.10.0)
|
||||
webpack: 5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
@@ -18765,9 +18768,9 @@ snapshots:
|
||||
'@webassemblyjs/ast': 1.11.6
|
||||
'@xtuc/long': 4.2.2
|
||||
|
||||
'@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))(webpack@5.90.3(webpack-cli@4.10.0))':
|
||||
'@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))(webpack@5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3)))':
|
||||
dependencies:
|
||||
webpack: 5.90.3(webpack-cli@4.10.0)
|
||||
webpack: 5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))
|
||||
webpack-cli: 4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3)
|
||||
|
||||
'@webpack-cli/info@1.5.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))':
|
||||
@@ -18820,9 +18823,9 @@ snapshots:
|
||||
acorn: 8.11.3
|
||||
acorn-walk: 8.2.0
|
||||
|
||||
acorn-import-assertions@1.9.0(acorn@8.10.0):
|
||||
acorn-import-assertions@1.9.0(acorn@8.11.3):
|
||||
dependencies:
|
||||
acorn: 8.10.0
|
||||
acorn: 8.11.3
|
||||
|
||||
acorn-jsx@5.3.2(acorn@7.4.1):
|
||||
dependencies:
|
||||
@@ -19266,14 +19269,14 @@ snapshots:
|
||||
schema-utils: 2.7.1
|
||||
webpack: 5.90.3
|
||||
|
||||
babel-loader@8.3.0(@babel/core@7.23.7)(webpack@5.90.3(webpack-cli@4.10.0)):
|
||||
babel-loader@8.3.0(@babel/core@7.23.7)(webpack@5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))):
|
||||
dependencies:
|
||||
'@babel/core': 7.23.7
|
||||
find-cache-dir: 3.3.2
|
||||
loader-utils: 2.0.4
|
||||
make-dir: 3.1.0
|
||||
schema-utils: 2.7.1
|
||||
webpack: 5.90.3(webpack-cli@4.10.0)
|
||||
webpack: 5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))
|
||||
|
||||
babel-plugin-add-module-exports@1.0.4: {}
|
||||
|
||||
@@ -20391,7 +20394,7 @@ snapshots:
|
||||
|
||||
copy-descriptor@0.1.1: {}
|
||||
|
||||
copy-webpack-plugin@9.1.0(webpack@5.90.3(webpack-cli@4.10.0)):
|
||||
copy-webpack-plugin@9.1.0(webpack@5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))):
|
||||
dependencies:
|
||||
fast-glob: 3.3.1
|
||||
glob-parent: 6.0.2
|
||||
@@ -20399,7 +20402,7 @@ snapshots:
|
||||
normalize-path: 3.0.0
|
||||
schema-utils: 3.3.0
|
||||
serialize-javascript: 6.0.1
|
||||
webpack: 5.90.3(webpack-cli@4.10.0)
|
||||
webpack: 5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))
|
||||
|
||||
core-js-compat@3.33.0:
|
||||
dependencies:
|
||||
@@ -29164,18 +29167,18 @@ snapshots:
|
||||
|
||||
term-size@2.2.1: {}
|
||||
|
||||
terser-webpack-plugin@5.3.10(webpack@5.90.3(webpack-cli@4.10.0)):
|
||||
terser-webpack-plugin@5.3.10(webpack@5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.22
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 3.3.0
|
||||
serialize-javascript: 6.0.1
|
||||
terser: 5.28.1
|
||||
webpack: 5.90.3(webpack-cli@4.10.0)
|
||||
webpack: 5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))
|
||||
|
||||
terser-webpack-plugin@5.3.10(webpack@5.90.3):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.22
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 3.3.0
|
||||
serialize-javascript: 6.0.1
|
||||
@@ -30039,7 +30042,7 @@ snapshots:
|
||||
webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3):
|
||||
dependencies:
|
||||
'@discoveryjs/json-ext': 0.5.7
|
||||
'@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))(webpack@5.90.3(webpack-cli@4.10.0))
|
||||
'@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))(webpack@5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3)))
|
||||
'@webpack-cli/info': 1.5.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))
|
||||
'@webpack-cli/serve': 1.7.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))
|
||||
colorette: 2.0.20
|
||||
@@ -30049,7 +30052,7 @@ snapshots:
|
||||
import-local: 3.1.0
|
||||
interpret: 2.2.0
|
||||
rechoir: 0.7.1
|
||||
webpack: 5.90.3(webpack-cli@4.10.0)
|
||||
webpack: 5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3))
|
||||
webpack-merge: 5.9.0
|
||||
optionalDependencies:
|
||||
webpack-bundle-analyzer: 4.10.1
|
||||
@@ -30091,9 +30094,9 @@ snapshots:
|
||||
'@webassemblyjs/ast': 1.11.6
|
||||
'@webassemblyjs/wasm-edit': 1.11.6
|
||||
'@webassemblyjs/wasm-parser': 1.11.6
|
||||
acorn: 8.10.0
|
||||
acorn-import-assertions: 1.9.0(acorn@8.10.0)
|
||||
browserslist: 4.22.2
|
||||
acorn: 8.11.3
|
||||
acorn-import-assertions: 1.9.0(acorn@8.11.3)
|
||||
browserslist: 4.23.0
|
||||
chrome-trace-event: 1.0.3
|
||||
enhanced-resolve: 5.15.0
|
||||
es-module-lexer: 1.3.1
|
||||
@@ -30115,16 +30118,16 @@ snapshots:
|
||||
- esbuild
|
||||
- uglify-js
|
||||
|
||||
webpack@5.90.3(webpack-cli@4.10.0):
|
||||
webpack@5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3)):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.5
|
||||
'@types/estree': 1.0.5
|
||||
'@webassemblyjs/ast': 1.11.6
|
||||
'@webassemblyjs/wasm-edit': 1.11.6
|
||||
'@webassemblyjs/wasm-parser': 1.11.6
|
||||
acorn: 8.10.0
|
||||
acorn-import-assertions: 1.9.0(acorn@8.10.0)
|
||||
browserslist: 4.22.2
|
||||
acorn: 8.11.3
|
||||
acorn-import-assertions: 1.9.0(acorn@8.11.3)
|
||||
browserslist: 4.23.0
|
||||
chrome-trace-event: 1.0.3
|
||||
enhanced-resolve: 5.15.0
|
||||
es-module-lexer: 1.3.1
|
||||
@@ -30138,7 +30141,7 @@ snapshots:
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 3.3.0
|
||||
tapable: 2.2.1
|
||||
terser-webpack-plugin: 5.3.10(webpack@5.90.3(webpack-cli@4.10.0))
|
||||
terser-webpack-plugin: 5.3.10(webpack@5.90.3(webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack@5.90.3)))
|
||||
watchpack: 2.4.0
|
||||
webpack-sources: 3.2.3
|
||||
optionalDependencies:
|
||||
|
||||
@@ -118,7 +118,7 @@ export const submitTypes = {
|
||||
[backEndProject]: 'project.backEnd',
|
||||
[pythonProject]: 'project.backEnd',
|
||||
[step]: 'step',
|
||||
[quiz]: 'quiz',
|
||||
[quiz]: 'tests',
|
||||
[backend]: 'backend',
|
||||
[modern]: 'tests',
|
||||
[video]: 'tests',
|
||||
|
||||
11
shared/utils/shuffle-array.ts
Normal file
11
shared/utils/shuffle-array.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/** Shuffle array using the Fisher–Yates shuffle algorithm */
|
||||
export const shuffleArray = <T>(arrToShuffle: Array<T>) => {
|
||||
const arr = [...arrToShuffle];
|
||||
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
|
||||
return arr;
|
||||
};
|
||||
Reference in New Issue
Block a user