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:
Tom
2023-08-09 03:29:49 -05:00
committed by GitHub
parent a57b3e94b3
commit 2eef45a209
24 changed files with 768 additions and 433 deletions
+31 -4
View File
@@ -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",
+1
View File
@@ -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',
+1
View File
@@ -25,6 +25,7 @@ export const actionTypes = createTypes(
'showCodeAlly',
'startExam',
'stopExam',
'clearExamResults',
'submitComplete',
'updateComplete',
'updateFailed',
+1
View File
@@ -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);
+20 -2
View File
@@ -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
+56 -1
View File
@@ -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;
}
+2
View File
@@ -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>
+29 -14
View File
@@ -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;
}
+203 -291
View File
@@ -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 };
+7
View File
@@ -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 {
+15
View File
@@ -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}`;
}
+1
View File
@@ -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,
@@ -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--
@@ -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--
+196 -21
View File
@@ -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