diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index 9346743815a..0ab91e5694d 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -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 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", diff --git a/client/package.json b/client/package.json index 66858b7f352..a7ab353d714 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/components/Flash/redux/flash-messages.ts b/client/src/components/Flash/redux/flash-messages.ts index 5f3f795fff2..4c9f4d1259b 100644 --- a/client/src/components/Flash/redux/flash-messages.ts +++ b/client/src/components/Flash/redux/flash-messages.ts @@ -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', diff --git a/client/src/redux/action-types.js b/client/src/redux/action-types.js index ee6a281ad1f..24a8fef7477 100644 --- a/client/src/redux/action-types.js +++ b/client/src/redux/action-types.js @@ -25,6 +25,7 @@ export const actionTypes = createTypes( 'showCodeAlly', 'startExam', 'stopExam', + 'clearExamResults', 'submitComplete', 'updateComplete', 'updateFailed', diff --git a/client/src/redux/actions.js b/client/src/redux/actions.js index 3f5dc58d508..1d1309b85a2 100644 --- a/client/src/redux/actions.js +++ b/client/src/redux/actions.js @@ -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); diff --git a/client/src/redux/index.js b/client/src/redux/index.js index ced9f0570f7..87b7696b801 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -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 diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index 64714eade13..527c6d64723 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -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; +} diff --git a/client/src/redux/selectors.js b/client/src/redux/selectors.js index 740cbea0d2a..f2cdca663ba 100644 --- a/client/src/redux/selectors.js +++ b/client/src/redux/selectors.js @@ -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 diff --git a/client/src/templates/Challenges/exam/components/exam-results.tsx b/client/src/templates/Challenges/exam/components/exam-results.tsx index c624eb9887a..f1b13ba84e3 100644 --- a/client/src/templates/Challenges/exam/components/exam-results.tsx +++ b/client/src/templates/Challenges/exam/components/exam-results.tsx @@ -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 ( -
-
-
{title} Results
+
+
+ {t('learn.exam.results-header', { title })}

+
{examResultsMessage}
+
-
Time: {timeInSeconds}
- {correctAnswers.length} of {results.length} correct answers |{' '} - {correctPercent}% + {t('learn.exam.question-results', { + n: numberOfCorrectAnswers, + q: numberOfQuestionsInExam + })} +
+
|
+
+ {t('learn.exam.percent-results', { + p: percentCorrect + })} +
+
|
+
+ {t('learn.exam.time', { t: formatSecondsToTime(examTimeInSeconds) })}
- - {results.map((result, index) => ( - <> -
-
- {result.correct ? : } -
- -
-
- Question {index + 1} -
-
{result.question}
-
Your Answer:
-
{result.answer}
-
-
- - - ))}
-
+ +
+
@@ -106,4 +111,4 @@ function ExamResults({ ExamResults.displayName = 'ExamResults'; -export default connect(mapStateToProps, mapDispatchToProps)(ExamResults); +export default ExamResults; diff --git a/client/src/templates/Challenges/exam/components/exit-exam-modal.tsx b/client/src/templates/Challenges/exam/components/exit-exam-modal.tsx new file mode 100644 index 00000000000..d8cd3676e36 --- /dev/null +++ b/client/src/templates/Challenges/exam/components/exit-exam-modal.tsx @@ -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 ( + + + + {t('learn.exam.exit-header')} + + + +
{t('learn.exam.exit')}
+
+ + + + +
+ ); +} + +ExitExamModal.displayName = 'ExitExamModal'; + +export default connect(mapStateToProps, mapDispatchToProps)(ExitExamModal); diff --git a/client/src/templates/Challenges/exam/components/finish-exam-modal.tsx b/client/src/templates/Challenges/exam/components/finish-exam-modal.tsx index 0590ca724a5..294d698c2fd 100644 --- a/client/src/templates/Challenges/exam/components/finish-exam-modal.tsx +++ b/client/src/templates/Challenges/exam/components/finish-exam-modal.tsx @@ -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 ( - Finish Exam + + {t('learn.exam.finish-header')} + -
- Are you sure? You will not be able to change any answers. Your results - will be final. -
+
{t('learn.exam.finish')}
diff --git a/client/src/templates/Challenges/exam/exam.css b/client/src/templates/Challenges/exam/exam.css index a0709d3c599..c2b551f8f81 100644 --- a/client/src/templates/Challenges/exam/exam.css +++ b/client/src/templates/Challenges/exam/exam.css @@ -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; } diff --git a/client/src/templates/Challenges/exam/show.tsx b/client/src/templates/Challenges/exam/show.tsx index 45ac3f773f1..54f962c8a88 100644 --- a/client/src/templates/Challenges/exam/show.tsx +++ b/client/src/templates/Challenges/exam/show.tsx @@ -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 { static displayName: string; private _container: RefObject | undefined; @@ -301,9 +143,9 @@ class ShowExam extends Component { super(props); this.state = { currentQuestionIndex: 0, - generatedExam: generatedExam, + generatedExamQuestions: [], examTimeInSeconds: 0, - userExam: [], + userExamQuestions: [], showResults: false }; @@ -312,8 +154,8 @@ class ShowExam extends Component { 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 { } 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 { 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 { }); }; - // 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 { challengeNode: { challenge: { block, + dashedName, description, fields: { blockName }, instructions, @@ -466,8 +330,10 @@ class ShowExam extends Component { } }, examInProgress, + examResults, completedChallenges, isChallengeCompleted, + openExitExamModal, openFinishExamModal, pageContext: { challengeMeta: { nextChallengePath, prevChallengePath } @@ -478,15 +344,22 @@ class ShowExam extends Component { 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 { const windowTitle = `${blockNameTitle} | freeCodeCamp.org`; const ariaLabel = t('aria.answer'); + // TODO: If already taken exam, show different messages + return examInProgress ? ( - {showResults ? ( + {examResults ? ( ) : (
{title}
| -
Time: {formatSecondsToTime(examTimeInSeconds)}
+
+ {t('learn.exam.time', { + t: formatSecondsToTime(examTimeInSeconds) + })} +
|
- Question {currentQuestionIndex + 1} of{' '} - {generatedExam.length} + {t('learn.exam.questions', { + n: currentQuestionIndex + 1, + t: generatedExamQuestions.length + })}

-
{generatedExam[currentQuestionIndex].question}
+ +
- {generatedExam[currentQuestionIndex].answers.map( - (option, answerIndex) => ( -
+ + + +
+ +
)} + @@ -621,11 +531,11 @@ class ShowExam extends Component { {qualifiedForExam ? ( -

{t('learn.qualified-for-exam')}

+

{t('learn.exam.qualified')}

) : ( -

{t('learn.not-qualified-for-exam')}

+

{t('learn.exam.not-qualified')}

    {missingPrequisites.map(({ title, id }) => ( @@ -673,8 +583,10 @@ export const query = graphql` challenge { block challengeType + dashedName description fields { + blockHashSlug blockName } helpCategory diff --git a/client/src/templates/Challenges/redux/action-types.js b/client/src/templates/Challenges/redux/action-types.js index 6ffbb0ab6ab..81509aba9af 100644 --- a/client/src/templates/Challenges/redux/action-types.js +++ b/client/src/templates/Challenges/redux/action-types.js @@ -32,7 +32,7 @@ export const actionTypes = createTypes( 'openModal', 'setIsAdvancing', 'setChapterSlug', - 'setExamResults', + 'setUserCompletedExam', 'previewMounted', 'projectPreviewMounted', 'storePortalWindow', diff --git a/client/src/templates/Challenges/redux/actions.js b/client/src/templates/Challenges/redux/actions.js index f5796c2ed9c..41dd9104e4e 100644 --- a/client/src/templates/Challenges/redux/actions.js +++ b/client/src/templates/Challenges/redux/actions.js @@ -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); diff --git a/client/src/templates/Challenges/redux/completion-epic.js b/client/src/templates/Challenges/redux/completion-epic.js index 9d8c70ab8fa..97e6281c966 100644 --- a/client/src/templates/Challenges/redux/completion-epic.js +++ b/client/src/templates/Challenges/redux/completion-epic.js @@ -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); } diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js index 8d4f058b936..d2b0025bf14 100644 --- a/client/src/templates/Challenges/redux/index.js +++ b/client/src/templates/Challenges/redux/index.js @@ -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, diff --git a/client/src/templates/Challenges/redux/selectors.js b/client/src/templates/Challenges/redux/selectors.js index fc389b8712b..b476e1ba0f5 100644 --- a/client/src/templates/Challenges/redux/selectors.js +++ b/client/src/templates/Challenges/redux/selectors.js @@ -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 }; diff --git a/client/src/utils/ajax.ts b/client/src/utils/ajax.ts index 3ed2d7e673e..7e92fbc1f74 100644 --- a/client/src/utils/ajax.ts +++ b/client/src/utils/ajax.ts @@ -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 { + return get(`/exam/${challengeId}`); +} + /** POST **/ interface Donation { diff --git a/client/src/utils/format-seconds.ts b/client/src/utils/format-seconds.ts new file mode 100644 index 00000000000..e931c43f336 --- /dev/null +++ b/client/src/utils/format-seconds.ts @@ -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}`; +} diff --git a/client/src/utils/tone/index.ts b/client/src/utils/tone/index.ts index 6f7b8d855f8..7cf38103f97 100644 --- a/client/src/utils/tone/index.ts +++ b/client/src/utils/tone/index.ts @@ -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, diff --git a/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/debug-c-sharp-console-applications/guided-project-debug-and-handle-exceptions-in-a-c-sharp-console-application-using-visual-studio-code.md b/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/debug-c-sharp-console-applications/guided-project-debug-and-handle-exceptions-in-a-c-sharp-console-application-using-visual-studio-code.md index b3419db1cb2..ad5acd4c259 100644 --- a/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/debug-c-sharp-console-applications/guided-project-debug-and-handle-exceptions-in-a-c-sharp-console-application-using-visual-studio-code.md +++ b/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/debug-c-sharp-console-applications/guided-project-debug-and-handle-exceptions-in-a-c-sharp-console-application-using-visual-studio-code.md @@ -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-- diff --git a/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam.md b/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam.md index 54119ff1976..dae16becaac 100644 --- a/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam.md +++ b/curriculum/challenges/english/19-foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam/foundational-c-sharp-with-microsoft-certification-exam.md @@ -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-- diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74586163390..a02fa95e77a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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