mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(client): make exam client work with new endpoints (#51125)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
@@ -80,7 +80,10 @@
|
||||
"tweet": "Tweet",
|
||||
"previous-question": "Previous question",
|
||||
"next-question": "Next question",
|
||||
"exit-exam": "Exit the exam",
|
||||
"exit": "Exit",
|
||||
"finish-exam": "Finish the exam",
|
||||
"finish": "Finish",
|
||||
"submit-exam-results": "Submit my results"
|
||||
},
|
||||
"landing": {
|
||||
@@ -353,6 +356,7 @@
|
||||
"console-output": "// console output",
|
||||
"sign-in-save": "Sign in to save your progress",
|
||||
"download-solution": "Download my solution",
|
||||
"download-results": "Download my results",
|
||||
"percent-complete": "{{percent}}% complete",
|
||||
"project-complete": "Completed {{completedChallengesInBlock}} of {{totalChallengesInBlock}} certification projects",
|
||||
"tried-rsa": "If you've already tried the <0>Read-Search-Ask</0> method, then you can ask for help on the freeCodeCamp forum.",
|
||||
@@ -403,9 +407,31 @@
|
||||
"if-getting-value": "If you're getting a lot out of freeCodeCamp, now is a great time to donate to support our charity's mission.",
|
||||
"building-a-university": "We're Building a Free Computer Science University Degree Program",
|
||||
"if-help-university": "We've already made a ton of progress. Support our charity with the long road ahead.",
|
||||
"qualified-for-exam": "Congratulations, you have completed all the requirements to qualify for the exam.",
|
||||
"not-qualified-for-exam": "You have not met the requirements to be eligible for the exam. To qualify, please complete the following challenges:",
|
||||
"preview-external-window": "Preview currently showing in external window."
|
||||
"preview-external-window": "Preview currently showing in external window.",
|
||||
"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:",
|
||||
"time": "Time: {{ t }}",
|
||||
"questions": "Question {{ n }} of {{ t }}",
|
||||
"passed": "Passed",
|
||||
"not-passed": "Not Passed",
|
||||
"number-of-questions": "Number of questions: {{ n }}",
|
||||
"correct-answers": "Correct answers: {{ n }}",
|
||||
"percent-correct": "Percent correct: {{ n }}%",
|
||||
"passed-message": "Congratulations! You passed the exam and can claim your certification.",
|
||||
"not-passed-message": "Sorry, but you did not answer enough questions correctly to pass the exam.",
|
||||
"results-header": "{{ title }} Results",
|
||||
"question-results": "You correctly answered {{ n }} out of {{ q }} questions",
|
||||
"percent-results": "{{ p }}% correct",
|
||||
"finish-header": "Finish Exam",
|
||||
"finish": "Are you sure you want to finish the exam? You will not be able to change any answers. Your results will be final.",
|
||||
"finish-yes": "Yes, I am finished",
|
||||
"finish-no": "No, I would like to continue the exam",
|
||||
"exit-header": "Exit Exam",
|
||||
"exit": "Are you sure you want to leave the exam? You will lose any progress you have made.",
|
||||
"exit-yes": "Yes, I want to leave the exam",
|
||||
"exit-no": "No, I would like to continue the exam"
|
||||
}
|
||||
},
|
||||
"donate": {
|
||||
"title": "Support our charity",
|
||||
@@ -665,7 +691,8 @@
|
||||
"code-save-less": "Slow Down! Your code was not saved. Try again in a few seconds.",
|
||||
"challenge-save-too-big": "Sorry, you cannot save your code. Your code is {{user-size}} bytes. We allow a maximum of {{max-size}} bytes. Please make your code smaller and try again or request assistance on https://forum.freecodecamp.org",
|
||||
"challenge-submit-too-big": "Sorry, you cannot submit your code. Your code is {{user-size}} bytes. We allow a maximum of {{max-size}} bytes. Please make your code smaller and try again or request assistance on https://forum.freecodecamp.org",
|
||||
"invalid-update-flag": "You are attempting to access forbidden resources. Please request assistance on https://forum.freecodecamp.org if this is a valid request."
|
||||
"invalid-update-flag": "You are attempting to access forbidden resources. Please request assistance on https://forum.freecodecamp.org if this is a valid request.",
|
||||
"generate-exam-error": "An error occurred trying to generate your exam."
|
||||
},
|
||||
"validation": {
|
||||
"max-characters": "There is a maximum limit of 288 characters, you have {{charsLeft}} left",
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
"jquery": "3.7.0",
|
||||
"lodash": "4.17.21",
|
||||
"lodash-es": "4.17.21",
|
||||
"micromark": "4.0.0",
|
||||
"monaco-editor": "0.28.1",
|
||||
"nanoid": "3.3.6",
|
||||
"normalize-url": "4.5.1",
|
||||
|
||||
@@ -13,6 +13,7 @@ export enum FlashMessages {
|
||||
CompleteProjectFirst = 'flash.complete-project-first',
|
||||
DeleteTokenErr = 'flash.delete-token-err',
|
||||
EmailValid = 'flash.email-valid',
|
||||
GenerateExamError = 'flash.generate-exam-error',
|
||||
HonestFirst = 'flash.honest-first',
|
||||
IncompleteSteps = 'flash.incomplete-steps',
|
||||
LocalCodeSaved = 'flash.local-code-saved',
|
||||
|
||||
@@ -25,6 +25,7 @@ export const actionTypes = createTypes(
|
||||
'showCodeAlly',
|
||||
'startExam',
|
||||
'stopExam',
|
||||
'clearExamResults',
|
||||
'submitComplete',
|
||||
'updateComplete',
|
||||
'updateFailed',
|
||||
|
||||
@@ -100,6 +100,7 @@ export const tryToShowCodeAlly = createAction(actionTypes.tryToShowCodeAlly);
|
||||
|
||||
export const startExam = createAction(actionTypes.startExam);
|
||||
export const stopExam = createAction(actionTypes.stopExam);
|
||||
export const clearExamResults = createAction(actionTypes.clearExamResults);
|
||||
|
||||
export const closeSignoutModal = createAction(actionTypes.closeSignoutModal);
|
||||
export const openSignoutModal = createAction(actionTypes.openSignoutModal);
|
||||
|
||||
@@ -302,7 +302,11 @@ export const reducer = handleActions(
|
||||
}
|
||||
}),
|
||||
[actionTypes.submitComplete]: (state, { payload }) => {
|
||||
const { submittedChallenge, savedChallenges } = payload;
|
||||
const {
|
||||
examResults = null,
|
||||
submittedChallenge,
|
||||
savedChallenges
|
||||
} = payload;
|
||||
let submittedchallenges = [
|
||||
{ ...submittedChallenge, completedDate: Date.now() }
|
||||
];
|
||||
@@ -325,7 +329,8 @@ export const reducer = handleActions(
|
||||
'id'
|
||||
),
|
||||
savedChallenges:
|
||||
savedChallenges ?? savedChallengesSelector(state[MainApp])
|
||||
savedChallenges ?? savedChallengesSelector(state[MainApp]),
|
||||
examResults
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -380,6 +385,19 @@ export const reducer = handleActions(
|
||||
examInProgress: false
|
||||
};
|
||||
},
|
||||
[actionTypes.clearExamResults]: state => {
|
||||
const { appUsername } = state;
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[appUsername]: {
|
||||
...state.user[appUsername],
|
||||
examResults: null
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[challengeTypes.challengeMounted]: (state, { payload }) => ({
|
||||
...state,
|
||||
currentChallengeId: payload
|
||||
|
||||
@@ -52,7 +52,12 @@ type Question = {
|
||||
answers: string[];
|
||||
solution: number;
|
||||
};
|
||||
type Fields = { slug: string; blockName: string; tests: Test[] };
|
||||
type Fields = {
|
||||
slug: string;
|
||||
blockHashSlug: string;
|
||||
blockName: string;
|
||||
tests: Test[];
|
||||
};
|
||||
type Required = {
|
||||
link: string;
|
||||
raw: boolean;
|
||||
@@ -347,3 +352,53 @@ export interface UserFetchState {
|
||||
errored: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// Exam Related Types:
|
||||
interface GeneratedExamAnswer {
|
||||
id: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
// Generated Exam (from API)
|
||||
export interface GeneratedExamQuestion {
|
||||
id: string;
|
||||
question: string;
|
||||
answers: GeneratedExamAnswer[];
|
||||
}
|
||||
|
||||
export interface GenerateExamResponse {
|
||||
error?: string;
|
||||
generatedExam?: GeneratedExamQuestion[];
|
||||
}
|
||||
|
||||
export interface GenerateExamResponseWithData {
|
||||
response: Response;
|
||||
data: GenerateExamResponse;
|
||||
}
|
||||
|
||||
// User Exam (null until they answer the question)
|
||||
interface UserExamAnswer {
|
||||
id: string | null;
|
||||
answer: string | null;
|
||||
}
|
||||
|
||||
export interface UserExamQuestion {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: UserExamAnswer;
|
||||
}
|
||||
|
||||
export interface UserExam {
|
||||
examTimeInSeconds: number;
|
||||
userExamQuestions: UserExamQuestion[];
|
||||
}
|
||||
|
||||
// Exam Results (from API)
|
||||
export interface GeneratedExamResults {
|
||||
numberOfCorrectAnswers: number;
|
||||
numberOfQuestionsInExam: number;
|
||||
percentCorrect: number;
|
||||
passingPercent: number;
|
||||
passed: boolean;
|
||||
examTimeInSeconds: number;
|
||||
}
|
||||
|
||||
@@ -85,6 +85,8 @@ export const examInProgressSelector = state => {
|
||||
return state[MainApp].examInProgress;
|
||||
};
|
||||
|
||||
export const examResultsSelector = state => userSelector(state).examResults;
|
||||
|
||||
export const userByNameSelector = username => state => {
|
||||
const { user } = state[MainApp];
|
||||
// return initial state empty user empty object instead of empty
|
||||
|
||||
@@ -1,103 +1,108 @@
|
||||
import { Button } from '@freecodecamp/react-bootstrap';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import GreenPass from '../../../../assets/icons/green-pass';
|
||||
import Fail from '../../../../assets/icons/fail';
|
||||
import Spacer from '../../../../components/helpers/spacer';
|
||||
import { submitChallenge } from '../../redux/actions';
|
||||
import { examResultsSelector } from '../../redux/selectors';
|
||||
|
||||
interface ExamResultQuestion {
|
||||
question: string;
|
||||
answer: string;
|
||||
correct: boolean;
|
||||
}
|
||||
|
||||
interface ExamResults {
|
||||
timeInSeconds: number;
|
||||
results: ExamResultQuestion[];
|
||||
}
|
||||
import { formatSecondsToTime } from '../../../../utils/format-seconds';
|
||||
import { GeneratedExamResults } from '../../../../redux/prop-types';
|
||||
|
||||
interface ExamResultsProps {
|
||||
examResults: ExamResults;
|
||||
submitExamResults: () => void;
|
||||
dashedName: string;
|
||||
examResults: GeneratedExamResults;
|
||||
exitExam: () => void;
|
||||
title: string;
|
||||
submitChallenge: () => void;
|
||||
}
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
examResultsSelector,
|
||||
(examResults: ExamResults) => ({
|
||||
examResults
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
submitChallenge
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
function ExamResults({
|
||||
examResults: { timeInSeconds, results },
|
||||
title,
|
||||
submitExamResults
|
||||
dashedName,
|
||||
examResults,
|
||||
exitExam,
|
||||
title
|
||||
}: ExamResultsProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const correctAnswers = results.filter(r => r.correct);
|
||||
const correctPercent = (correctAnswers.length / results.length) * 100;
|
||||
const {
|
||||
numberOfCorrectAnswers,
|
||||
examTimeInSeconds,
|
||||
numberOfQuestionsInExam,
|
||||
passed,
|
||||
percentCorrect
|
||||
} = examResults;
|
||||
|
||||
// keep this formatting
|
||||
const downloadContent = `${title}: ${
|
||||
passed ? t('learn.exam.passed') : t('learn.exam.not-passed')
|
||||
}
|
||||
|
||||
${t('learn.exam.number-of-questions', { n: numberOfQuestionsInExam })}
|
||||
${t('learn.exam.correct-answers', { n: numberOfCorrectAnswers })}
|
||||
${t('learn.exam.percent-correct', { n: percentCorrect })}
|
||||
${t('learn.exam.time', { t: formatSecondsToTime(examTimeInSeconds) })}
|
||||
`;
|
||||
|
||||
const blob = new Blob([downloadContent], {
|
||||
type: 'text/plain'
|
||||
});
|
||||
const downloadURL = URL.createObjectURL(blob);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
URL.revokeObjectURL(downloadURL);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const examResultsMessage = passed
|
||||
? t('learn.exam.passed-message')
|
||||
: t('learn.exam.not-passed-message');
|
||||
|
||||
// TODO: Add share button
|
||||
return (
|
||||
<div className='exam-wrapper'>
|
||||
<div className='exam-header'>
|
||||
<div>{title} Results</div>
|
||||
<div className='exam-results-wrapper'>
|
||||
<div className='exam-results-header'>
|
||||
{t('learn.exam.results-header', { title })}
|
||||
</div>
|
||||
<hr />
|
||||
<Spacer size='medium' />
|
||||
|
||||
<div className='exam-results-message'>{examResultsMessage}</div>
|
||||
<Spacer size='medium' />
|
||||
<div className='exam-results'>
|
||||
<div>Time: {timeInSeconds}</div>
|
||||
<div>
|
||||
{correctAnswers.length} of {results.length} correct answers |{' '}
|
||||
{correctPercent}%
|
||||
{t('learn.exam.question-results', {
|
||||
n: numberOfCorrectAnswers,
|
||||
q: numberOfQuestionsInExam
|
||||
})}
|
||||
</div>
|
||||
<div>|</div>
|
||||
<div>
|
||||
{t('learn.exam.percent-results', {
|
||||
p: percentCorrect
|
||||
})}
|
||||
</div>
|
||||
<div>|</div>
|
||||
<div>
|
||||
{t('learn.exam.time', { t: formatSecondsToTime(examTimeInSeconds) })}
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
{results.map((result, index) => (
|
||||
<>
|
||||
<div className='exam-result' key={index}>
|
||||
<div className='exam-result-icon'>
|
||||
{result.correct ? <GreenPass /> : <Fail />}
|
||||
</div>
|
||||
|
||||
<div className='exam-result-questions'>
|
||||
<div className='exam-result-question-label'>
|
||||
Question {index + 1}
|
||||
</div>
|
||||
<div className='exam-result-question'>{result.question}</div>
|
||||
<div className='exam-result-answer-label'>Your Answer:</div>
|
||||
<div className='exam-result-answer'>{result.answer}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
<div className='exam-buttons'>
|
||||
<Spacer size='medium' />
|
||||
<div className='exam-results-buttons'>
|
||||
<Button
|
||||
block={true}
|
||||
className='exam-button'
|
||||
bsStyle='primary'
|
||||
data-cy='submit-exam-results'
|
||||
onClick={submitExamResults}
|
||||
className='btn-invert'
|
||||
download={`${dashedName}.txt`}
|
||||
href={downloadURL}
|
||||
>
|
||||
{t('buttons.submit-exam-results')}
|
||||
{t('learn.download-results')}
|
||||
</Button>
|
||||
<Button
|
||||
block={true}
|
||||
bsStyle='primary'
|
||||
data-cy='exit-exam'
|
||||
onClick={exitExam}
|
||||
>
|
||||
{t('buttons.exit')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,4 +111,4 @@ function ExamResults({
|
||||
|
||||
ExamResults.displayName = 'ExamResults';
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ExamResults);
|
||||
export default ExamResults;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Package Utilities
|
||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Local Utilities
|
||||
import { closeModal } from '../../redux/actions';
|
||||
import { isExitExamModalOpenSelector } from '../../redux/selectors';
|
||||
|
||||
// Types
|
||||
interface ExitExamModalProps {
|
||||
closeExitExamModal: () => void;
|
||||
isExitExamModalOpen: boolean;
|
||||
exitExam: () => void;
|
||||
}
|
||||
|
||||
// Redux Setup
|
||||
const mapStateToProps = createSelector(
|
||||
isExitExamModalOpenSelector,
|
||||
(isExitExamModalOpen: boolean) => ({
|
||||
isExitExamModalOpen
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
closeExitExamModal: () => closeModal('exitExam')
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
// Component
|
||||
function ExitExamModal({
|
||||
closeExitExamModal,
|
||||
isExitExamModalOpen,
|
||||
exitExam
|
||||
}: ExitExamModalProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
animation={false}
|
||||
dialogClassName='exit-exam-modal'
|
||||
keyboard={true}
|
||||
onHide={closeExitExamModal}
|
||||
show={isExitExamModalOpen}
|
||||
>
|
||||
<Modal.Header className='exit-exam-modal-header' closeButton={true}>
|
||||
<Modal.Title className='text-center'>
|
||||
{t('learn.exam.exit-header')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className='reset-modal-body'>
|
||||
<div className='text-center'>{t('learn.exam.exit')}</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className='reset-modal-footer'>
|
||||
<Button
|
||||
data-cy='exit-exam-modal-confirm'
|
||||
block={true}
|
||||
bsStyle='danger'
|
||||
onClick={exitExam}
|
||||
>
|
||||
{t('learn.exam.exit-yes')}
|
||||
</Button>
|
||||
<Button
|
||||
data-cy='exit-exam-modal-deny'
|
||||
block={true}
|
||||
bsStyle='primary'
|
||||
onClick={closeExitExamModal}
|
||||
>
|
||||
{t('learn.exam.exit-no')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ExitExamModal.displayName = 'ExitExamModal';
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ExitExamModal);
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Local Utilities
|
||||
import { closeModal } from '../../redux/actions';
|
||||
@@ -38,6 +39,8 @@ function FinishExamModal({
|
||||
isFinishExamModalOpen,
|
||||
finishExam
|
||||
}: FinishExamModalProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
animation={false}
|
||||
@@ -47,13 +50,12 @@ function FinishExamModal({
|
||||
show={isFinishExamModalOpen}
|
||||
>
|
||||
<Modal.Header className='finish-exam-modal-header' closeButton={true}>
|
||||
<Modal.Title className='text-center'>Finish Exam</Modal.Title>
|
||||
<Modal.Title className='text-center'>
|
||||
{t('learn.exam.finish-header')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className='reset-modal-body'>
|
||||
<div className='text-center'>
|
||||
Are you sure? You will not be able to change any answers. Your results
|
||||
will be final.
|
||||
</div>
|
||||
<div className='text-center'>{t('learn.exam.finish')}</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className='reset-modal-footer'>
|
||||
<Button
|
||||
@@ -63,7 +65,7 @@ function FinishExamModal({
|
||||
bsStyle='primary'
|
||||
onClick={finishExam}
|
||||
>
|
||||
Yes, I am finished
|
||||
{t('learn.exam.finish-yes')}
|
||||
</Button>
|
||||
<Button
|
||||
data-cy='finish-exam-modal-deny'
|
||||
@@ -72,7 +74,7 @@ function FinishExamModal({
|
||||
bsStyle='primary'
|
||||
onClick={closeFinishExamModal}
|
||||
>
|
||||
No, I would like to continue the exam
|
||||
{t('learn.exam.finish-no')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.exam-wrapper {
|
||||
.exam-wrapper,
|
||||
.exam-results-wrapper {
|
||||
margin-top: 60px;
|
||||
padding: 25px;
|
||||
background-color: var(--primary-background);
|
||||
@@ -6,12 +7,14 @@
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.exam-wrapper {
|
||||
.exam-wrapper,
|
||||
.exam-results-wrapper {
|
||||
margin-top: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.exam-header {
|
||||
.exam-header,
|
||||
.exam-results-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
@@ -28,12 +31,30 @@
|
||||
|
||||
.exam-answer-label {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.exam-answer-label > .line-numbers,
|
||||
.exam-questions > .line-numbers {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.exam-answers pre,
|
||||
.exam-questions pre {
|
||||
color: var(--tertiary-color);
|
||||
border: 1px solid var(--secondary-color);
|
||||
background-color: var(--quaternary-background);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.exam-answer-label p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.exam-answer-input-hidden {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
@@ -74,17 +95,11 @@
|
||||
max-width: 40%;
|
||||
}
|
||||
|
||||
.exam-result {
|
||||
display: flex;
|
||||
.exam-results-message {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.exam-result-icon > svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.exam-result-questions {
|
||||
.exam-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-inline-start: 30px;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package Utilities
|
||||
import { Alert, Grid, Col, Row, Button } from '@freecodecamp/react-bootstrap';
|
||||
import { graphql } from 'gatsby';
|
||||
import { graphql, navigate } from 'gatsby';
|
||||
import React, { Component, RefObject } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import type { TFunction } from 'i18next';
|
||||
@@ -9,6 +9,7 @@ import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { micromark } from 'micromark';
|
||||
|
||||
// Local Utilities
|
||||
import Spacer from '../../../components/helpers/spacer';
|
||||
@@ -18,11 +19,12 @@ import PrismFormatted from '../components/prism-formatted';
|
||||
import CompletionModal from '../components/completion-modal';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import Hotkeys from '../components/hotkeys';
|
||||
import { startExam, stopExam } from '../../../redux/actions';
|
||||
import { clearExamResults, startExam, stopExam } from '../../../redux/actions';
|
||||
import {
|
||||
completedChallengesSelector,
|
||||
isSignedInSelector,
|
||||
examInProgressSelector
|
||||
examInProgressSelector,
|
||||
examResultsSelector
|
||||
} from '../../../redux/selectors';
|
||||
import {
|
||||
challengeMounted,
|
||||
@@ -30,19 +32,26 @@ import {
|
||||
openModal,
|
||||
closeModal,
|
||||
submitChallenge,
|
||||
setExamResults,
|
||||
setUserCompletedExam,
|
||||
updateSolutionFormValues
|
||||
} from '../redux/actions';
|
||||
import { getGenerateExam } from '../../../utils/ajax';
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
import { createFlashMessage } from '../../../components/Flash/redux';
|
||||
import {
|
||||
ChallengeNode,
|
||||
ChallengeMeta,
|
||||
CompletedChallenge
|
||||
CompletedChallenge,
|
||||
UserExamQuestion,
|
||||
UserExam,
|
||||
GeneratedExamResults,
|
||||
GeneratedExamQuestion
|
||||
} from '../../../redux/prop-types';
|
||||
import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
|
||||
import { formatSecondsToTime } from '../../../utils/format-seconds';
|
||||
import ExitExamModal from './components/exit-exam-modal';
|
||||
import FinishExamModal from './components/finish-exam-modal';
|
||||
import ExamResults from './components/exam-results';
|
||||
|
||||
import './exam.css';
|
||||
|
||||
// Redux
|
||||
@@ -51,16 +60,19 @@ const mapStateToProps = createSelector(
|
||||
isChallengeCompletedSelector,
|
||||
isSignedInSelector,
|
||||
examInProgressSelector,
|
||||
examResultsSelector,
|
||||
(
|
||||
completedChallenges: CompletedChallenge[],
|
||||
isChallengeCompleted: boolean,
|
||||
isSignedIn: boolean,
|
||||
examInProgress: boolean
|
||||
examInProgress: boolean,
|
||||
examResults: GeneratedExamResults | null
|
||||
) => ({
|
||||
completedChallenges,
|
||||
isChallengeCompleted,
|
||||
isSignedIn,
|
||||
examInProgress
|
||||
examInProgress,
|
||||
examResults
|
||||
})
|
||||
);
|
||||
|
||||
@@ -69,11 +81,14 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
{
|
||||
challengeMounted,
|
||||
createFlashMessage,
|
||||
openExitExamModal: () => openModal('exitExam'),
|
||||
closeExitExamModal: () => closeModal('exitExam'),
|
||||
openFinishExamModal: () => openModal('finishExam'),
|
||||
closeFinishExamModal: () => closeModal('finishExam'),
|
||||
startExam,
|
||||
stopExam,
|
||||
setExamResults,
|
||||
setUserCompletedExam,
|
||||
clearExamResults,
|
||||
submitChallenge,
|
||||
updateChallengeMeta,
|
||||
updateSolutionFormValues
|
||||
@@ -85,11 +100,15 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
interface ShowExamProps {
|
||||
challengeMounted: (arg0: string) => void;
|
||||
completedChallenges: CompletedChallenge[];
|
||||
clearExamResults: () => void;
|
||||
createFlashMessage: typeof createFlashMessage;
|
||||
data: { challengeNode: ChallengeNode };
|
||||
examInProgress: boolean;
|
||||
examResults: GeneratedExamResults | null;
|
||||
isChallengeCompleted: boolean;
|
||||
isSignedIn: boolean;
|
||||
openExitExamModal: () => void;
|
||||
closeExitExamModal: () => void;
|
||||
openFinishExamModal: () => void;
|
||||
closeFinishExamModal: () => void;
|
||||
pageContext: {
|
||||
@@ -99,199 +118,22 @@ interface ShowExamProps {
|
||||
startExam: () => void;
|
||||
stopExam: () => void;
|
||||
submitChallenge: () => void;
|
||||
setExamResults: (arg0: ExamResults) => void;
|
||||
setUserCompletedExam: (arg0: UserExam) => void;
|
||||
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
||||
}
|
||||
|
||||
interface ShowExamState {
|
||||
currentQuestionIndex: number;
|
||||
examTimeInSeconds: number;
|
||||
generatedExam: GeneratedExamQuestion[];
|
||||
userExam: UserExamQuestion[];
|
||||
generatedExamQuestions: GeneratedExamQuestion[];
|
||||
userExamQuestions: UserExamQuestion[];
|
||||
showResults: boolean;
|
||||
}
|
||||
|
||||
interface GeneratedExamQuestion {
|
||||
question: string;
|
||||
answers: string[];
|
||||
function convertMd(md: string): string {
|
||||
return micromark(md);
|
||||
}
|
||||
|
||||
interface UserExamQuestion {
|
||||
question: string;
|
||||
answer: string | null;
|
||||
}
|
||||
|
||||
interface ExamResultQuestion {
|
||||
question: string;
|
||||
answer: string;
|
||||
correct: boolean;
|
||||
}
|
||||
|
||||
interface ExamResults {
|
||||
timeInSeconds: number;
|
||||
results: ExamResultQuestion[];
|
||||
}
|
||||
|
||||
const examInDatabase = [
|
||||
{
|
||||
question: 'Which of the following is a programming language?',
|
||||
wrongAnswers: ['Apple', 'Orange', 'Banana', 'Mango'],
|
||||
correctAnswer: 'Python'
|
||||
},
|
||||
{
|
||||
question: 'What does CSS stand for?',
|
||||
wrongAnswers: [
|
||||
'Computer Style Sheets',
|
||||
'Complete Style Sheets',
|
||||
'Cool Style Sheets',
|
||||
'Creative Style Sheets'
|
||||
],
|
||||
correctAnswer: 'Cascading Style Sheets'
|
||||
},
|
||||
{
|
||||
question: 'What is the extension for a JavaScript file?',
|
||||
wrongAnswers: ['.txt', '.doc', '.html', '.css'],
|
||||
correctAnswer: '.js'
|
||||
},
|
||||
{
|
||||
question: 'What is the purpose of the "if" statement in programming?',
|
||||
wrongAnswers: [
|
||||
'To repeat a set of instructions',
|
||||
'To define a function',
|
||||
'To assign a value to a variable',
|
||||
'To declare a loop'
|
||||
],
|
||||
correctAnswer: 'To check a condition'
|
||||
},
|
||||
{
|
||||
question:
|
||||
'What is the symbol used to represent addition in most programming languages?',
|
||||
wrongAnswers: ['-', '*', '=', '%'],
|
||||
correctAnswer: '+'
|
||||
},
|
||||
{
|
||||
question: 'Which of the following is NOT a programming language?',
|
||||
wrongAnswers: ['Java', 'Ruby', 'Swift', 'PHP'],
|
||||
correctAnswer: 'Spanish'
|
||||
}
|
||||
];
|
||||
|
||||
Object.freeze(examInDatabase);
|
||||
|
||||
// TODO: move helper functions to utility file
|
||||
// helper functions
|
||||
function shuffleArray(
|
||||
array: string[] | GeneratedExamQuestion[]
|
||||
): string[] | GeneratedExamQuestion[] {
|
||||
let currentIndex = array.length,
|
||||
randomIndex;
|
||||
|
||||
while (currentIndex != 0) {
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
[array[currentIndex], array[randomIndex]] = [
|
||||
array[randomIndex],
|
||||
array[currentIndex]
|
||||
];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
function formatSecondsToTime(s: number) {
|
||||
const hourInSeconds = 60 * 60;
|
||||
const minuteInSeconds = 60;
|
||||
const h = Math.floor(s / hourInSeconds);
|
||||
s -= h * hourInSeconds;
|
||||
|
||||
const minutes = Math.floor(s / minuteInSeconds);
|
||||
s -= minutes * minuteInSeconds;
|
||||
|
||||
const mm = minutes < 10 && h >= 1 ? `0${minutes}` : minutes;
|
||||
const seconds = s % 60;
|
||||
const ss = seconds < 10 ? `0${seconds}` : seconds;
|
||||
|
||||
return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
|
||||
}
|
||||
|
||||
// TODO: generate exam on server
|
||||
function generateExam(): GeneratedExamQuestion[] {
|
||||
const NUMBER_OF_ANSWERS_PER_QUESTION = 4;
|
||||
const NUMBER_OF_QUESTIONS_IN_EXAM = 5;
|
||||
const generatedExam: GeneratedExamQuestion[] = [];
|
||||
const examFromDatabase = Array.from(examInDatabase);
|
||||
|
||||
while (generatedExam.length < NUMBER_OF_QUESTIONS_IN_EXAM) {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * (examFromDatabase.length - 1)
|
||||
);
|
||||
const randomQuestion = examFromDatabase.splice(randomIndex, 1)[0];
|
||||
const wrongAnswers = randomQuestion.wrongAnswers;
|
||||
const answers = [randomQuestion.correctAnswer];
|
||||
|
||||
while (answers.length < NUMBER_OF_ANSWERS_PER_QUESTION) {
|
||||
const index = Math.floor(Math.random() * (wrongAnswers.length - 1));
|
||||
const randomAnswer = wrongAnswers.splice(index, 1)[0];
|
||||
answers.push(randomAnswer);
|
||||
}
|
||||
|
||||
const newExamQuestion: GeneratedExamQuestion = {
|
||||
question: randomQuestion.question,
|
||||
answers: shuffleArray(answers) as string[]
|
||||
};
|
||||
|
||||
generatedExam.push(newExamQuestion);
|
||||
}
|
||||
|
||||
return shuffleArray(generatedExam) as GeneratedExamQuestion[];
|
||||
}
|
||||
|
||||
/* const exampleGeneratedExam = [
|
||||
{
|
||||
"question": "What is the extension for a JavaScript file?",
|
||||
"answers": ['.txt', '.js', '.html', '.css']
|
||||
},
|
||||
...rest_of_questions
|
||||
]*/
|
||||
|
||||
/* const exampleUserExam = [
|
||||
{
|
||||
"question": "What is the extension for a JavaScript file?",
|
||||
"answer": ".doc"
|
||||
},
|
||||
...rest_of_questions
|
||||
]*/
|
||||
|
||||
/* const exampleExamResults = [
|
||||
{
|
||||
"question": "What is the extension for a JavaScript file?",
|
||||
"answer": ".doc",
|
||||
"correct": false
|
||||
}
|
||||
...rest_of_questions
|
||||
]
|
||||
*/
|
||||
|
||||
/* example item added to completedChalleges array
|
||||
{
|
||||
"id": "645147516c245de4d11eb7ba",
|
||||
"completedDate": 1644532946064,
|
||||
"exam": {
|
||||
"completionTimeInSeconds": number,
|
||||
"results": [
|
||||
{
|
||||
"question": "What is the extension for a JavaScript file?",
|
||||
"answer": ".doc",
|
||||
"correct": false
|
||||
},
|
||||
...rest_of_questions
|
||||
]
|
||||
}
|
||||
}*/
|
||||
|
||||
const generatedExam = generateExam();
|
||||
Object.freeze(generatedExam);
|
||||
|
||||
class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
static displayName: string;
|
||||
private _container: RefObject<HTMLElement> | undefined;
|
||||
@@ -301,9 +143,9 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
super(props);
|
||||
this.state = {
|
||||
currentQuestionIndex: 0,
|
||||
generatedExam: generatedExam,
|
||||
generatedExamQuestions: [],
|
||||
examTimeInSeconds: 0,
|
||||
userExam: [],
|
||||
userExamQuestions: [],
|
||||
showResults: false
|
||||
};
|
||||
|
||||
@@ -312,8 +154,8 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
this.goToNextQuestion = this.goToNextQuestion.bind(this);
|
||||
this.selectAnswer = this.selectAnswer.bind(this);
|
||||
this.finishExam = this.finishExam.bind(this);
|
||||
this.createExamResults = this.createExamResults.bind(this);
|
||||
this.submitExamResults = this.submitExamResults.bind(this);
|
||||
this.exitExam = this.exitExam.bind(this);
|
||||
this.cleanUp = this.cleanUp.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -339,11 +181,8 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.cleanUp();
|
||||
this.props.stopExam();
|
||||
clearInterval(this.timerInterval);
|
||||
window.removeEventListener('beforeunload', this.stopWindowClose);
|
||||
window.removeEventListener('unload', this.stopWindowClose);
|
||||
window.removeEventListener('popstate', this.stopBrowserBack);
|
||||
}
|
||||
|
||||
stopWindowClose = (event: Event) => {
|
||||
@@ -357,36 +196,63 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
alert(this.props.t('misc.navigation-warning'));
|
||||
};
|
||||
|
||||
runExam = () => {
|
||||
runExam = async () => {
|
||||
// TODO: show loader
|
||||
// TODO: fetch exam from server/database
|
||||
const newExam = this.state.generatedExam.map(q => {
|
||||
return { question: q.question, answer: null };
|
||||
});
|
||||
const {
|
||||
createFlashMessage,
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: { id: challengeId }
|
||||
}
|
||||
}
|
||||
} = this.props;
|
||||
|
||||
this.timerInterval = setInterval(() => {
|
||||
this.setState(state => ({
|
||||
examTimeInSeconds: state.examTimeInSeconds + 1
|
||||
}));
|
||||
}, 1000);
|
||||
const generateExamResponse = await getGenerateExam(challengeId);
|
||||
const { response, data } = generateExamResponse;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
userExam: newExam
|
||||
},
|
||||
this.props.startExam
|
||||
);
|
||||
if (response.status === 200) {
|
||||
const { generatedExam = [] } = data;
|
||||
const emptyUserExamQuestions = generatedExam.map(q => {
|
||||
return {
|
||||
id: q.id,
|
||||
question: q.question,
|
||||
answer: { id: null, answer: null }
|
||||
};
|
||||
}) as UserExamQuestion[];
|
||||
|
||||
window.addEventListener('beforeunload', this.stopWindowClose);
|
||||
window.addEventListener('unload', this.stopWindowClose);
|
||||
window.addEventListener('popstate', this.stopBrowserBack);
|
||||
this.setState(
|
||||
{
|
||||
generatedExamQuestions: generatedExam,
|
||||
userExamQuestions: emptyUserExamQuestions
|
||||
},
|
||||
() => {
|
||||
this.timerInterval = setInterval(() => {
|
||||
this.setState({
|
||||
examTimeInSeconds: this.state.examTimeInSeconds + 1
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
this.props.startExam();
|
||||
|
||||
window.addEventListener('beforeunload', this.stopWindowClose);
|
||||
window.addEventListener('unload', this.stopWindowClose);
|
||||
window.addEventListener('popstate', this.stopBrowserBack);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
createFlashMessage({
|
||||
type: 'danger',
|
||||
message: FlashMessages.GenerateExamError
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
selectAnswer = (index: number, option: string): void => {
|
||||
const newExam = Array.from(this.state.userExam);
|
||||
newExam[index].answer = option;
|
||||
selectAnswer = (index: number, id: string, answer: string): void => {
|
||||
const newUserExamQuestions = Array.from(this.state.userExamQuestions);
|
||||
newUserExamQuestions[index].answer.id = id;
|
||||
newUserExamQuestions[index].answer.answer = answer;
|
||||
this.setState({
|
||||
userExam: newExam
|
||||
userExamQuestions: newUserExamQuestions
|
||||
});
|
||||
};
|
||||
|
||||
@@ -402,51 +268,48 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: have server check the exam
|
||||
createExamResults = () => {
|
||||
// TODO: show loader
|
||||
const { setExamResults } = this.props;
|
||||
const { userExam, examTimeInSeconds } = this.state;
|
||||
|
||||
const results: ExamResultQuestion[] = [];
|
||||
|
||||
userExam.forEach(userQuestion => {
|
||||
const questionInDb = examInDatabase.find(
|
||||
dbQuestion => userQuestion.question === dbQuestion.question
|
||||
);
|
||||
|
||||
const questionResult = {
|
||||
question: userQuestion.question,
|
||||
answer: userQuestion.answer,
|
||||
correct: questionInDb?.correctAnswer === userQuestion.answer
|
||||
};
|
||||
|
||||
results.push(questionResult as ExamResultQuestion);
|
||||
});
|
||||
|
||||
const examResults = {
|
||||
timeInSeconds: examTimeInSeconds,
|
||||
results
|
||||
};
|
||||
|
||||
setExamResults(examResults);
|
||||
};
|
||||
|
||||
finishExam = () => {
|
||||
cleanUp = () => {
|
||||
clearInterval(this.timerInterval);
|
||||
this.props.closeFinishExamModal();
|
||||
this.createExamResults();
|
||||
this.setState({
|
||||
showResults: true
|
||||
examTimeInSeconds: 0,
|
||||
currentQuestionIndex: 0
|
||||
});
|
||||
};
|
||||
|
||||
submitExamResults = () => {
|
||||
window.removeEventListener('beforeunload', this.stopWindowClose);
|
||||
window.removeEventListener('unload', this.stopWindowClose);
|
||||
window.removeEventListener('popstate', this.stopBrowserBack);
|
||||
this.props.submitChallenge();
|
||||
this.props.stopExam();
|
||||
|
||||
this.props.clearExamResults();
|
||||
this.props.closeExitExamModal();
|
||||
this.props.closeFinishExamModal();
|
||||
};
|
||||
|
||||
finishExam = () => {
|
||||
// TODO: show loader
|
||||
this.cleanUp();
|
||||
|
||||
const { setUserCompletedExam, submitChallenge } = this.props;
|
||||
const { userExamQuestions, examTimeInSeconds } = this.state;
|
||||
|
||||
setUserCompletedExam({ userExamQuestions, examTimeInSeconds });
|
||||
submitChallenge();
|
||||
};
|
||||
|
||||
exitExam = () => {
|
||||
this.cleanUp();
|
||||
|
||||
const {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
fields: { blockHashSlug }
|
||||
}
|
||||
}
|
||||
},
|
||||
stopExam
|
||||
} = this.props;
|
||||
stopExam();
|
||||
void navigate(blockHashSlug);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -455,6 +318,7 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
block,
|
||||
dashedName,
|
||||
description,
|
||||
fields: { blockName },
|
||||
instructions,
|
||||
@@ -466,8 +330,10 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
}
|
||||
},
|
||||
examInProgress,
|
||||
examResults,
|
||||
completedChallenges,
|
||||
isChallengeCompleted,
|
||||
openExitExamModal,
|
||||
openFinishExamModal,
|
||||
pageContext: {
|
||||
challengeMeta: { nextChallengePath, prevChallengePath }
|
||||
@@ -478,15 +344,22 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
const {
|
||||
examTimeInSeconds,
|
||||
currentQuestionIndex,
|
||||
generatedExam,
|
||||
userExam,
|
||||
showResults
|
||||
generatedExamQuestions,
|
||||
userExamQuestions
|
||||
} = this.state;
|
||||
|
||||
const missingPrequisites = prerequisites.filter(
|
||||
prerequisite =>
|
||||
!completedChallenges.find(({ id }) => prerequisite.id === id)
|
||||
);
|
||||
type Prerequisite = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
let missingPrequisites: Prerequisite[] = [];
|
||||
if (prerequisites) {
|
||||
missingPrequisites = prerequisites?.filter(
|
||||
prerequisite =>
|
||||
!completedChallenges.find(({ id }) => prerequisite.id === id)
|
||||
);
|
||||
}
|
||||
|
||||
const qualifiedForExam = missingPrequisites.length === 0;
|
||||
|
||||
@@ -496,61 +369,78 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
const windowTitle = `${blockNameTitle} | freeCodeCamp.org`;
|
||||
const ariaLabel = t('aria.answer');
|
||||
|
||||
// TODO: If already taken exam, show different messages
|
||||
|
||||
return examInProgress ? (
|
||||
<Grid>
|
||||
<Row>
|
||||
<Spacer size='medium' />
|
||||
<Col md={10} mdOffset={1} sm={10} smOffset={1} xs={12}>
|
||||
{showResults ? (
|
||||
{examResults ? (
|
||||
<ExamResults
|
||||
dashedName={dashedName}
|
||||
title={title}
|
||||
submitExamResults={this.submitExamResults}
|
||||
examResults={examResults}
|
||||
exitExam={this.exitExam}
|
||||
/>
|
||||
) : (
|
||||
<div className='exam-wrapper'>
|
||||
<div className='exam-header'>
|
||||
<div>{title}</div>
|
||||
<span>|</span>
|
||||
<div> Time: {formatSecondsToTime(examTimeInSeconds)}</div>
|
||||
<div>
|
||||
{t('learn.exam.time', {
|
||||
t: formatSecondsToTime(examTimeInSeconds)
|
||||
})}
|
||||
</div>
|
||||
<span>|</span>
|
||||
<div>
|
||||
Question {currentQuestionIndex + 1} of{' '}
|
||||
{generatedExam.length}
|
||||
{t('learn.exam.questions', {
|
||||
n: currentQuestionIndex + 1,
|
||||
t: generatedExamQuestions.length
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<Spacer size='medium' />
|
||||
|
||||
<div className='exam-questions'>
|
||||
<div>{generatedExam[currentQuestionIndex].question}</div>
|
||||
<PrismFormatted
|
||||
text={convertMd(
|
||||
generatedExamQuestions[currentQuestionIndex].question
|
||||
)}
|
||||
/>
|
||||
|
||||
<Spacer size='large' />
|
||||
<div className='exam-answers'>
|
||||
{generatedExam[currentQuestionIndex].answers.map(
|
||||
(option, answerIndex) => (
|
||||
<label className='exam-answer-label' key={answerIndex}>
|
||||
{generatedExamQuestions[currentQuestionIndex].answers.map(
|
||||
({ answer, id }) => (
|
||||
<label className='exam-answer-label' key={id}>
|
||||
<input
|
||||
aria-label={ariaLabel}
|
||||
checked={
|
||||
userExam[currentQuestionIndex].answer === option
|
||||
userExamQuestions[currentQuestionIndex].answer
|
||||
.id === id
|
||||
}
|
||||
className='exam-answer-input-hidden'
|
||||
name='exam'
|
||||
name={id}
|
||||
onChange={() =>
|
||||
this.selectAnswer(currentQuestionIndex, option)
|
||||
this.selectAnswer(
|
||||
currentQuestionIndex,
|
||||
id,
|
||||
answer
|
||||
)
|
||||
}
|
||||
type='radio'
|
||||
value={option}
|
||||
value={id}
|
||||
/>{' '}
|
||||
<span className='exam-answer-input-visible'>
|
||||
{userExam[currentQuestionIndex].answer ===
|
||||
option ? (
|
||||
{userExamQuestions[currentQuestionIndex].answer
|
||||
.id === id ? (
|
||||
<span className='exam-answer-input-selected' />
|
||||
) : null}
|
||||
</span>
|
||||
<PrismFormatted
|
||||
className={'exam-answer'}
|
||||
text={option}
|
||||
/>
|
||||
<PrismFormatted text={convertMd(answer)} />
|
||||
</label>
|
||||
)
|
||||
)}
|
||||
@@ -570,10 +460,13 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
{t('buttons.previous-question')}
|
||||
</Button>
|
||||
|
||||
{currentQuestionIndex === generatedExam.length - 1 ? (
|
||||
{currentQuestionIndex ===
|
||||
generatedExamQuestions.length - 1 ? (
|
||||
<Button
|
||||
block={true}
|
||||
disabled={!userExam[currentQuestionIndex].answer}
|
||||
disabled={
|
||||
!userExamQuestions[currentQuestionIndex].answer.id
|
||||
}
|
||||
className='exam-button'
|
||||
bsStyle='primary'
|
||||
data-cy='finish-exam'
|
||||
@@ -584,7 +477,9 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
) : (
|
||||
<Button
|
||||
block={true}
|
||||
disabled={!userExam[currentQuestionIndex].answer}
|
||||
disabled={
|
||||
!userExamQuestions[currentQuestionIndex].answer.id
|
||||
}
|
||||
className='exam-button'
|
||||
bsStyle='primary'
|
||||
data-cy='next-exam-question'
|
||||
@@ -594,9 +489,24 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Spacer size='medium' />
|
||||
|
||||
<div className='exam-buttons'>
|
||||
<Button
|
||||
block={true}
|
||||
className='exam-button'
|
||||
bsStyle='primary'
|
||||
data-cy='exit-exam'
|
||||
onClick={openExitExamModal}
|
||||
>
|
||||
{t('buttons.exit-exam')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<ExitExamModal exitExam={this.exitExam} />
|
||||
<FinishExamModal finishExam={this.finishExam} />
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -621,11 +531,11 @@ class ShowExam extends Component<ShowExamProps, ShowExamState> {
|
||||
|
||||
{qualifiedForExam ? (
|
||||
<Alert id='qualified-for-exam' bsStyle='info'>
|
||||
<p>{t('learn.qualified-for-exam')}</p>
|
||||
<p>{t('learn.exam.qualified')}</p>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert id='not-qualified-for-exam' bsStyle='danger'>
|
||||
<p>{t('learn.not-qualified-for-exam')}</p>
|
||||
<p>{t('learn.exam.not-qualified')}</p>
|
||||
<Spacer size='small' />
|
||||
<ul>
|
||||
{missingPrequisites.map(({ title, id }) => (
|
||||
@@ -673,8 +583,10 @@ export const query = graphql`
|
||||
challenge {
|
||||
block
|
||||
challengeType
|
||||
dashedName
|
||||
description
|
||||
fields {
|
||||
blockHashSlug
|
||||
blockName
|
||||
}
|
||||
helpCategory
|
||||
|
||||
@@ -32,7 +32,7 @@ export const actionTypes = createTypes(
|
||||
'openModal',
|
||||
'setIsAdvancing',
|
||||
'setChapterSlug',
|
||||
'setExamResults',
|
||||
'setUserCompletedExam',
|
||||
'previewMounted',
|
||||
'projectPreviewMounted',
|
||||
'storePortalWindow',
|
||||
|
||||
@@ -51,7 +51,9 @@ export const noStoredCodeFound = createAction(actionTypes.noStoredCodeFound);
|
||||
export const saveEditorContent = createAction(actionTypes.saveEditorContent);
|
||||
export const setIsAdvancing = createAction(actionTypes.setIsAdvancing);
|
||||
export const setChapterSlug = createAction(actionTypes.setChapterSlug);
|
||||
export const setExamResults = createAction(actionTypes.setExamResults);
|
||||
export const setUserCompletedExam = createAction(
|
||||
actionTypes.setUserCompletedExam
|
||||
);
|
||||
|
||||
export const closeModal = createAction(actionTypes.closeModal);
|
||||
export const openModal = createAction(actionTypes.openModal);
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
challengeFilesSelector,
|
||||
challengeMetaSelector,
|
||||
challengeTestsSelector,
|
||||
examResultsSelector,
|
||||
userCompletedExamSelector,
|
||||
projectFormValuesSelector,
|
||||
isBlockNewlyCompletedSelector
|
||||
} from './selectors';
|
||||
@@ -53,7 +53,12 @@ function postChallenge(update, username) {
|
||||
const saveChallenge = postUpdate$(update).pipe(
|
||||
retry(3),
|
||||
switchMap(({ data }) => {
|
||||
const { savedChallenges, points, isTrophyMissing } = data;
|
||||
const {
|
||||
savedChallenges,
|
||||
points,
|
||||
isTrophyMissing,
|
||||
examResults = {}
|
||||
} = data;
|
||||
const payloadWithClientProperties = {
|
||||
...omit(update.payload, ['files'])
|
||||
};
|
||||
@@ -73,7 +78,8 @@ function postChallenge(update, username) {
|
||||
points,
|
||||
...payloadWithClientProperties
|
||||
},
|
||||
savedChallenges: mapFilesToChallengeFiles(savedChallenges)
|
||||
savedChallenges: mapFilesToChallengeFiles(savedChallenges),
|
||||
examResults
|
||||
}),
|
||||
updateComplete(),
|
||||
submitChallengeComplete()
|
||||
@@ -181,10 +187,11 @@ const submitters = {
|
||||
function submitExam(type, state) {
|
||||
// TODO: verify shape of examResults?
|
||||
if (type === actionTypes.submitChallenge) {
|
||||
const { id } = challengeMetaSelector(state);
|
||||
const examResults = examResultsSelector(state);
|
||||
const { id, challengeType } = challengeMetaSelector(state);
|
||||
const userCompletedExam = userCompletedExamSelector(state);
|
||||
|
||||
const { username } = userSelector(state);
|
||||
const challengeInfo = { id, examResults };
|
||||
const challengeInfo = { id, challengeType, userCompletedExam };
|
||||
|
||||
const update = {
|
||||
endpoint: '/exam-challenge-completed',
|
||||
@@ -209,7 +216,6 @@ export default function completionEpic(action$, state$) {
|
||||
block,
|
||||
blockHashSlug
|
||||
} = challengeMetaSelector(state);
|
||||
|
||||
let submitter = () => of({ type: 'no-user-signed-in' });
|
||||
if (
|
||||
!(challengeType in submitTypes) ||
|
||||
@@ -244,7 +250,9 @@ export default function completionEpic(action$, state$) {
|
||||
mergeMap(x => of(x, setRenderStartTime(Date.now()))),
|
||||
tap(res => {
|
||||
if (res.type !== submitActionTypes.updateFailed) {
|
||||
navigate(pathToNavigateTo);
|
||||
if (challengeType !== challengeTypes.exam) {
|
||||
navigate(pathToNavigateTo);
|
||||
}
|
||||
} else {
|
||||
createFlashMessage(standardErrorMessage);
|
||||
}
|
||||
|
||||
@@ -29,10 +29,7 @@ const initialState = {
|
||||
},
|
||||
challengeTests: [],
|
||||
consoleOut: [],
|
||||
examResults: {
|
||||
timeInSeconds: 0,
|
||||
results: []
|
||||
},
|
||||
userCompletedExam: null,
|
||||
hasCompletedBlock: false,
|
||||
isBuildEnabled: true,
|
||||
isResetting: false,
|
||||
@@ -42,6 +39,7 @@ const initialState = {
|
||||
help: false,
|
||||
video: false,
|
||||
reset: false,
|
||||
exitExam: false,
|
||||
finishExam: false,
|
||||
projectPreview: false,
|
||||
shortcuts: false
|
||||
@@ -214,9 +212,9 @@ export const reducer = handleActions(
|
||||
...state,
|
||||
chapterSlug: payload
|
||||
}),
|
||||
[actionTypes.setExamResults]: (state, { payload }) => ({
|
||||
[actionTypes.setUserCompletedExam]: (state, { payload }) => ({
|
||||
...state,
|
||||
examResults: payload
|
||||
userCompletedExam: payload
|
||||
}),
|
||||
[actionTypes.closeModal]: (state, { payload }) => ({
|
||||
...state,
|
||||
|
||||
@@ -28,6 +28,7 @@ export const isCompletionModalOpenSelector = state =>
|
||||
export const isHelpModalOpenSelector = state => state[ns].modal.help;
|
||||
export const isVideoModalOpenSelector = state => state[ns].modal.video;
|
||||
export const isResetModalOpenSelector = state => state[ns].modal.reset;
|
||||
export const isExitExamModalOpenSelector = state => state[ns].modal.exitExam;
|
||||
export const isFinishExamModalOpenSelector = state =>
|
||||
state[ns].modal.finishExam;
|
||||
export const isProjectPreviewModalOpenSelector = state =>
|
||||
@@ -46,7 +47,7 @@ export const chapterSlugSelector = state => state[ns].chapterSlug;
|
||||
export const portalDocumentSelector = state => state[ns].portalWindow?.document;
|
||||
export const portalWindowSelector = state => state[ns].portalWindow;
|
||||
|
||||
export const examResultsSelector = state => state[ns].examResults;
|
||||
export const userCompletedExamSelector = state => state[ns].userCompletedExam;
|
||||
export const challengeDataSelector = state => {
|
||||
const { challengeType } = challengeMetaSelector(state);
|
||||
let challengeData = { challengeType };
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
ChallengeFile,
|
||||
ChallengeFiles,
|
||||
CompletedChallenge,
|
||||
GenerateExamResponseWithData,
|
||||
SavedChallenge,
|
||||
SavedChallengeFile,
|
||||
User
|
||||
@@ -213,6 +214,12 @@ export function getUsernameExists(
|
||||
return get(`/api/users/exists?username=${username}`);
|
||||
}
|
||||
|
||||
export function getGenerateExam(
|
||||
challengeId: string
|
||||
): Promise<GenerateExamResponseWithData> {
|
||||
return get(`/exam/${challengeId}`);
|
||||
}
|
||||
|
||||
/** POST **/
|
||||
|
||||
interface Donation {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export function formatSecondsToTime(s: number) {
|
||||
const hourInSeconds = 60 * 60;
|
||||
const minuteInSeconds = 60;
|
||||
const h = Math.floor(s / hourInSeconds);
|
||||
s -= h * hourInSeconds;
|
||||
|
||||
const minutes = Math.floor(s / minuteInSeconds);
|
||||
s -= minutes * minuteInSeconds;
|
||||
|
||||
const mm = minutes < 10 && h >= 1 ? `0${minutes}` : minutes;
|
||||
const seconds = s % 60;
|
||||
const ss = seconds < 10 ? `0${seconds}` : seconds;
|
||||
|
||||
return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ const toneUrls = {
|
||||
[FlashMessages.CompleteProjectFirst]: TRY_AGAIN,
|
||||
[FlashMessages.DeleteTokenErr]: TRY_AGAIN,
|
||||
[FlashMessages.EmailValid]: CHAL_COMP,
|
||||
[FlashMessages.GenerateExamError]: TRY_AGAIN,
|
||||
[FlashMessages.HonestFirst]: TRY_AGAIN,
|
||||
[FlashMessages.IncompleteSteps]: TRY_AGAIN,
|
||||
[FlashMessages.LocalCodeSaved]: CHAL_COMP,
|
||||
|
||||
+4
@@ -22,8 +22,12 @@ When is an `ArgumentOutOfRangeException` exception thrown?
|
||||
|
||||
An `ArgumentOutOfRangeException` exception is thrown when an attempt is made to index an array outside the bounds of the array.
|
||||
|
||||
---
|
||||
|
||||
An `ArgumentOutOfRangeException` exception is thrown when the value of an argument is outside the allowable range of values as defined by the method.
|
||||
|
||||
---
|
||||
|
||||
An `ArgumentOutOfRangeException` exception is thrown when an attempt is made to store a value of one type in an array of another type.
|
||||
|
||||
## --video-solution--
|
||||
|
||||
+2
-2
@@ -24,9 +24,9 @@ Pass this exam to earn your Foundational C# with Microsoft Certification. Before
|
||||
|
||||
- You must complete the entire exam in one session.
|
||||
- Exiting the exam at any point before you are finished, will result in the loss of your progress.
|
||||
- The exam consists of multiple-choice questions.
|
||||
- There is no time limit, but your total time taken will be recorded.
|
||||
- Complete the exam and answer a sufficent number of question correctly to earn your certification.
|
||||
- The exam consists of 80 multiple-choice questions.
|
||||
- Complete the exam and correctly answer at least 70% of the questions to earn your certification.
|
||||
|
||||
# --instructions--
|
||||
|
||||
|
||||
Generated
+196
-21
@@ -629,6 +629,9 @@ importers:
|
||||
lodash-es:
|
||||
specifier: 4.17.21
|
||||
version: 4.17.21
|
||||
micromark:
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
monaco-editor:
|
||||
specifier: 0.28.1
|
||||
version: 0.28.1
|
||||
@@ -16763,6 +16766,7 @@ packages:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
supports-color: 8.1.1
|
||||
dev: true
|
||||
|
||||
/debug@4.3.1:
|
||||
resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==}
|
||||
@@ -17093,7 +17097,7 @@ packages:
|
||||
'@types/tmp': 0.0.33
|
||||
application-config-path: 0.1.1
|
||||
command-exists: 1.2.9
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
eol: 0.9.1
|
||||
get-port: 3.2.0
|
||||
glob: 7.2.3
|
||||
@@ -17108,6 +17112,12 @@ packages:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/devlop@1.1.0:
|
||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
dev: false
|
||||
|
||||
/devtools-protocol@0.0.901419:
|
||||
resolution: {integrity: sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==}
|
||||
dev: true
|
||||
@@ -18012,7 +18022,7 @@ packages:
|
||||
/eslint-import-resolver-node@0.3.7:
|
||||
resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==}
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
is-core-module: 2.12.1
|
||||
resolve: 1.22.2
|
||||
transitivePeerDependencies:
|
||||
@@ -18063,7 +18073,7 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@4.9.5)
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
eslint: 7.32.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.28.0)(eslint@8.46.0)
|
||||
@@ -18092,7 +18102,7 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@4.9.5)
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
eslint: 8.46.0
|
||||
eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.28.0)(eslint@8.46.0)
|
||||
transitivePeerDependencies:
|
||||
@@ -18120,7 +18130,7 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@4.9.5)
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
eslint: 8.46.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.28.0)(eslint@8.46.0)
|
||||
@@ -18194,7 +18204,7 @@ packages:
|
||||
array-includes: 3.1.6
|
||||
array.prototype.flat: 1.3.1
|
||||
array.prototype.flatmap: 1.3.1
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
doctrine: 2.1.0
|
||||
eslint: 7.32.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
@@ -18227,7 +18237,7 @@ packages:
|
||||
array.prototype.findlastindex: 1.2.2
|
||||
array.prototype.flat: 1.3.1
|
||||
array.prototype.flatmap: 1.3.1
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.46.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
@@ -18322,7 +18332,7 @@ packages:
|
||||
minimatch: 3.1.2
|
||||
object.entries: 1.1.6
|
||||
object.fromentries: 2.0.6
|
||||
semver: 6.3.1
|
||||
semver: 6.3.0
|
||||
|
||||
/eslint-plugin-jsx-a11y@6.7.1(eslint@8.46.0):
|
||||
resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==}
|
||||
@@ -19523,7 +19533,6 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
debug: 2.2.0
|
||||
dev: false
|
||||
|
||||
/follow-redirects@1.15.2(debug@3.2.7):
|
||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||
@@ -19534,7 +19543,7 @@ packages:
|
||||
debug:
|
||||
optional: true
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
|
||||
/follow-redirects@1.15.2(debug@4.3.4):
|
||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||
@@ -20356,7 +20365,7 @@ packages:
|
||||
css-minimizer-webpack-plugin: 2.0.0(webpack@5.88.2)
|
||||
css.escape: 1.5.1
|
||||
date-fns: 2.30.0
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
deepmerge: 4.3.0
|
||||
del: 5.1.0
|
||||
detect-port: 1.5.1
|
||||
@@ -21577,7 +21586,7 @@ packages:
|
||||
engines: {node: '>=8.0.0'}
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
follow-redirects: 1.15.2(debug@4.3.4)
|
||||
follow-redirects: 1.15.2(debug@2.2.0)
|
||||
requires-port: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
@@ -24553,7 +24562,7 @@ packages:
|
||||
dependencies:
|
||||
async: 0.9.2
|
||||
commondir: 1.0.1
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
lodash: 4.17.21
|
||||
semver: 5.7.1
|
||||
strong-globalize: 4.1.3
|
||||
@@ -24566,7 +24575,7 @@ packages:
|
||||
resolution: {integrity: sha512-vDRR4gqkvGOEXh5yL383xGuGxUW9xtF+NCY6/lJu1VAgupKltZxEx3Vw+L3nsGvQrlkJTSmiK3jk72qxkoBtbw==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
lodash: 4.17.21
|
||||
loopback-swagger: 5.9.0
|
||||
strong-globalize: 4.1.3
|
||||
@@ -24581,7 +24590,7 @@ packages:
|
||||
dependencies:
|
||||
async: 2.6.4
|
||||
bson: 1.1.6
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
loopback-connector: 4.11.1
|
||||
mongodb: 3.6.9
|
||||
strong-globalize: 4.1.3
|
||||
@@ -24625,7 +24634,7 @@ packages:
|
||||
dependencies:
|
||||
async: 2.6.4
|
||||
bluebird: 3.7.2
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
depd: 1.1.2
|
||||
inflection: 1.13.4
|
||||
lodash: 4.17.21
|
||||
@@ -24649,7 +24658,7 @@ packages:
|
||||
resolution: {integrity: sha512-p0qSzuuX7eATe5Bxy+RqCj3vSfSFfdCtqyf3yuC+DpchMvgal33XlhEi2UmywyK/Ym28oVnZxxWmfrwFMzSwLQ==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
@@ -24659,7 +24668,7 @@ packages:
|
||||
engines: {node: '>=8.9'}
|
||||
dependencies:
|
||||
async: 2.6.4
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
strong-globalize: 4.1.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -24670,7 +24679,7 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
async: 2.6.4
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
ejs: 2.7.4
|
||||
lodash: 4.17.21
|
||||
strong-globalize: 4.1.3
|
||||
@@ -25366,6 +25375,27 @@ packages:
|
||||
micromark-util-types: 1.0.2
|
||||
uvu: 0.5.6
|
||||
|
||||
/micromark-core-commonmark@2.0.0:
|
||||
resolution: {integrity: sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==}
|
||||
dependencies:
|
||||
decode-named-character-reference: 1.0.2
|
||||
devlop: 1.1.0
|
||||
micromark-factory-destination: 2.0.0
|
||||
micromark-factory-label: 2.0.0
|
||||
micromark-factory-space: 2.0.0
|
||||
micromark-factory-title: 2.0.0
|
||||
micromark-factory-whitespace: 2.0.0
|
||||
micromark-util-character: 2.0.1
|
||||
micromark-util-chunked: 2.0.0
|
||||
micromark-util-classify-character: 2.0.0
|
||||
micromark-util-html-tag-name: 2.0.0
|
||||
micromark-util-normalize-identifier: 2.0.0
|
||||
micromark-util-resolve-all: 2.0.0
|
||||
micromark-util-subtokenize: 2.0.0
|
||||
micromark-util-symbol: 2.0.0
|
||||
micromark-util-types: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-extension-directive@1.4.0:
|
||||
resolution: {integrity: sha512-8uJN4N2hfhxc0I2Mdya+HZ35D0fyBnHn66aVnHawLj0Nd22Poqgqw3N0vTdYOsNwwrshfMLlPDKtLfEeq4lxgw==}
|
||||
dependencies:
|
||||
@@ -25498,6 +25528,14 @@ packages:
|
||||
micromark-util-symbol: 1.0.1
|
||||
micromark-util-types: 1.0.2
|
||||
|
||||
/micromark-factory-destination@2.0.0:
|
||||
resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==}
|
||||
dependencies:
|
||||
micromark-util-character: 2.0.1
|
||||
micromark-util-symbol: 2.0.0
|
||||
micromark-util-types: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-factory-label@1.0.2:
|
||||
resolution: {integrity: sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==}
|
||||
dependencies:
|
||||
@@ -25506,6 +25544,15 @@ packages:
|
||||
micromark-util-types: 1.0.2
|
||||
uvu: 0.5.6
|
||||
|
||||
/micromark-factory-label@2.0.0:
|
||||
resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==}
|
||||
dependencies:
|
||||
devlop: 1.1.0
|
||||
micromark-util-character: 2.0.1
|
||||
micromark-util-symbol: 2.0.0
|
||||
micromark-util-types: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-factory-mdx-expression@1.0.7:
|
||||
resolution: {integrity: sha512-QAdFbkQagTZ/eKb8zDGqmjvgevgJH3+aQpvvKrXWxNJp3o8/l2cAbbrBd0E04r0Gx6nssPpqWIjnbHFvZu5qsQ==}
|
||||
dependencies:
|
||||
@@ -25524,6 +25571,13 @@ packages:
|
||||
micromark-util-character: 1.1.0
|
||||
micromark-util-types: 1.0.2
|
||||
|
||||
/micromark-factory-space@2.0.0:
|
||||
resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==}
|
||||
dependencies:
|
||||
micromark-util-character: 2.0.1
|
||||
micromark-util-types: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-factory-title@1.0.2:
|
||||
resolution: {integrity: sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==}
|
||||
dependencies:
|
||||
@@ -25533,6 +25587,15 @@ packages:
|
||||
micromark-util-types: 1.0.2
|
||||
uvu: 0.5.6
|
||||
|
||||
/micromark-factory-title@2.0.0:
|
||||
resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==}
|
||||
dependencies:
|
||||
micromark-factory-space: 2.0.0
|
||||
micromark-util-character: 2.0.1
|
||||
micromark-util-symbol: 2.0.0
|
||||
micromark-util-types: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-factory-whitespace@1.0.0:
|
||||
resolution: {integrity: sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==}
|
||||
dependencies:
|
||||
@@ -25541,17 +25604,39 @@ packages:
|
||||
micromark-util-symbol: 1.0.1
|
||||
micromark-util-types: 1.0.2
|
||||
|
||||
/micromark-factory-whitespace@2.0.0:
|
||||
resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==}
|
||||
dependencies:
|
||||
micromark-factory-space: 2.0.0
|
||||
micromark-util-character: 2.0.1
|
||||
micromark-util-symbol: 2.0.0
|
||||
micromark-util-types: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-util-character@1.1.0:
|
||||
resolution: {integrity: sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==}
|
||||
dependencies:
|
||||
micromark-util-symbol: 1.0.1
|
||||
micromark-util-types: 1.0.2
|
||||
|
||||
/micromark-util-character@2.0.1:
|
||||
resolution: {integrity: sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==}
|
||||
dependencies:
|
||||
micromark-util-symbol: 2.0.0
|
||||
micromark-util-types: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-util-chunked@1.0.0:
|
||||
resolution: {integrity: sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==}
|
||||
dependencies:
|
||||
micromark-util-symbol: 1.0.1
|
||||
|
||||
/micromark-util-chunked@2.0.0:
|
||||
resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==}
|
||||
dependencies:
|
||||
micromark-util-symbol: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-util-classify-character@1.0.0:
|
||||
resolution: {integrity: sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==}
|
||||
dependencies:
|
||||
@@ -25559,17 +25644,38 @@ packages:
|
||||
micromark-util-symbol: 1.0.1
|
||||
micromark-util-types: 1.0.2
|
||||
|
||||
/micromark-util-classify-character@2.0.0:
|
||||
resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==}
|
||||
dependencies:
|
||||
micromark-util-character: 2.0.1
|
||||
micromark-util-symbol: 2.0.0
|
||||
micromark-util-types: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-util-combine-extensions@1.0.0:
|
||||
resolution: {integrity: sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==}
|
||||
dependencies:
|
||||
micromark-util-chunked: 1.0.0
|
||||
micromark-util-types: 1.0.2
|
||||
|
||||
/micromark-util-combine-extensions@2.0.0:
|
||||
resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==}
|
||||
dependencies:
|
||||
micromark-util-chunked: 2.0.0
|
||||
micromark-util-types: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-util-decode-numeric-character-reference@1.0.0:
|
||||
resolution: {integrity: sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==}
|
||||
dependencies:
|
||||
micromark-util-symbol: 1.0.1
|
||||
|
||||
/micromark-util-decode-numeric-character-reference@2.0.0:
|
||||
resolution: {integrity: sha512-pIgcsGxpHEtTG/rPJRz/HOLSqp5VTuIIjXlPI+6JSDlK2oljApusG6KzpS8AF0ENUMCHlC/IBb5B9xdFiVlm5Q==}
|
||||
dependencies:
|
||||
micromark-util-symbol: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-util-decode-string@1.0.2:
|
||||
resolution: {integrity: sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==}
|
||||
dependencies:
|
||||
@@ -25581,6 +25687,10 @@ packages:
|
||||
/micromark-util-encode@1.0.1:
|
||||
resolution: {integrity: sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==}
|
||||
|
||||
/micromark-util-encode@2.0.0:
|
||||
resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==}
|
||||
dev: false
|
||||
|
||||
/micromark-util-events-to-acorn@1.2.1:
|
||||
resolution: {integrity: sha512-mkg3BaWlw6ZTkQORrKVBW4o9ICXPxLtGz51vml5mQpKFdo9vqIX68CAx5JhTOdjQyAHH7JFmm4rh8toSPQZUmg==}
|
||||
dependencies:
|
||||
@@ -25595,16 +25705,32 @@ packages:
|
||||
/micromark-util-html-tag-name@1.1.0:
|
||||
resolution: {integrity: sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==}
|
||||
|
||||
/micromark-util-html-tag-name@2.0.0:
|
||||
resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==}
|
||||
dev: false
|
||||
|
||||
/micromark-util-normalize-identifier@1.0.0:
|
||||
resolution: {integrity: sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==}
|
||||
dependencies:
|
||||
micromark-util-symbol: 1.0.1
|
||||
|
||||
/micromark-util-normalize-identifier@2.0.0:
|
||||
resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==}
|
||||
dependencies:
|
||||
micromark-util-symbol: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-util-resolve-all@1.0.0:
|
||||
resolution: {integrity: sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==}
|
||||
dependencies:
|
||||
micromark-util-types: 1.0.2
|
||||
|
||||
/micromark-util-resolve-all@2.0.0:
|
||||
resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==}
|
||||
dependencies:
|
||||
micromark-util-types: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-util-sanitize-uri@1.1.0:
|
||||
resolution: {integrity: sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==}
|
||||
dependencies:
|
||||
@@ -25612,6 +25738,14 @@ packages:
|
||||
micromark-util-encode: 1.0.1
|
||||
micromark-util-symbol: 1.0.1
|
||||
|
||||
/micromark-util-sanitize-uri@2.0.0:
|
||||
resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==}
|
||||
dependencies:
|
||||
micromark-util-character: 2.0.1
|
||||
micromark-util-encode: 2.0.0
|
||||
micromark-util-symbol: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-util-subtokenize@1.0.2:
|
||||
resolution: {integrity: sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==}
|
||||
dependencies:
|
||||
@@ -25620,12 +25754,29 @@ packages:
|
||||
micromark-util-types: 1.0.2
|
||||
uvu: 0.5.6
|
||||
|
||||
/micromark-util-subtokenize@2.0.0:
|
||||
resolution: {integrity: sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==}
|
||||
dependencies:
|
||||
devlop: 1.1.0
|
||||
micromark-util-chunked: 2.0.0
|
||||
micromark-util-symbol: 2.0.0
|
||||
micromark-util-types: 2.0.0
|
||||
dev: false
|
||||
|
||||
/micromark-util-symbol@1.0.1:
|
||||
resolution: {integrity: sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==}
|
||||
|
||||
/micromark-util-symbol@2.0.0:
|
||||
resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==}
|
||||
dev: false
|
||||
|
||||
/micromark-util-types@1.0.2:
|
||||
resolution: {integrity: sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==}
|
||||
|
||||
/micromark-util-types@2.0.0:
|
||||
resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==}
|
||||
dev: false
|
||||
|
||||
/micromark@2.11.4:
|
||||
resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==}
|
||||
dependencies:
|
||||
@@ -25658,6 +25809,30 @@ packages:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/micromark@4.0.0:
|
||||
resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==}
|
||||
dependencies:
|
||||
'@types/debug': 4.1.7
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
decode-named-character-reference: 1.0.2
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.0
|
||||
micromark-factory-space: 2.0.0
|
||||
micromark-util-character: 2.0.1
|
||||
micromark-util-chunked: 2.0.0
|
||||
micromark-util-combine-extensions: 2.0.0
|
||||
micromark-util-decode-numeric-character-reference: 2.0.0
|
||||
micromark-util-encode: 2.0.0
|
||||
micromark-util-normalize-identifier: 2.0.0
|
||||
micromark-util-resolve-all: 2.0.0
|
||||
micromark-util-sanitize-uri: 2.0.0
|
||||
micromark-util-subtokenize: 2.0.0
|
||||
micromark-util-symbol: 2.0.0
|
||||
micromark-util-types: 2.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/micromatch@3.1.10:
|
||||
resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -34272,7 +34447,7 @@ packages:
|
||||
/webpack-virtual-modules@0.2.2:
|
||||
resolution: {integrity: sha512-kDUmfm3BZrei0y+1NTHJInejzxfhtU8eDj2M7OKb2IWrPFAeO1SOH2KuQ68MSZu9IGEHcxbkKKR1v18FrUSOmA==}
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -34280,7 +34455,7 @@ packages:
|
||||
/webpack-virtual-modules@0.3.2:
|
||||
resolution: {integrity: sha512-RXQXioY6MhzM4CNQwmBwKXYgBs6ulaiQ8bkNQEl2J6Z+V+s7lgl/wGvaI/I0dLnYKB8cKsxQc17QOAVIphPLDw==}
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
||||
Reference in New Issue
Block a user