diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index 0ab91e5694d..180a811ac19 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -206,6 +206,7 @@
"project-name": "Project Name",
"solution": "Solution",
"solution-for": "Solution for {{projectTitle}}",
+ "results-for": "Results for {{projectTitle}}",
"my-profile": "My profile",
"my-name": "My name",
"my-location": "My location",
diff --git a/client/src/client-only-routes/show-project-links.tsx b/client/src/client-only-routes/show-project-links.tsx
index 9852fcb8058..41852c9bed6 100644
--- a/client/src/client-only-routes/show-project-links.tsx
+++ b/client/src/client-only-routes/show-project-links.tsx
@@ -14,6 +14,7 @@ import {
import { SolutionDisplayWidget } from '../components/solution-display-widget';
import ProjectPreviewModal from '../templates/Challenges/components/project-preview-modal';
+import ExamResultsModal from '../components/SolutionViewer/exam-results-modal';
import { openModal } from '../templates/Challenges/redux/actions';
@@ -79,6 +80,15 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
openModal('projectPreview');
};
+ const showExamResults = () => {
+ setSolutionState({
+ projectTitle,
+ completedChallenge: completedProject,
+ showCode: false
+ });
+ openModal('examResults');
+ };
+
return (
{
displayContext='certification'
showUserCode={showUserCode}
showProjectPreview={showProjectPreview}
+ showExamResults={showExamResults}
>
);
};
@@ -137,6 +148,7 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
user: { username }
} = props;
const { completedChallenge, showCode, projectTitle } = solutionState;
+ const examResults = completedChallenge?.examResults;
const challengeData: CompletedChallenge | null = completedChallenge
? {
@@ -189,6 +201,8 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
previewTitle={projectTitle}
showProjectPreview={true}
/>
+
+
If you suspect that any of these projects violate the{' '}
void;
+ isOpen: boolean;
+};
+
+const mapStateToProps = (state: unknown) => ({
+ isOpen: isExamResultsModalOpenSelector(state) as boolean
+});
+
+const mapDispatchToProps = {
+ closeModal
+};
+
+const ExamResultsModal = ({
+ projectTitle,
+ examResults = {
+ numberOfCorrectAnswers: 0,
+ examTimeInSeconds: 0,
+ numberOfQuestionsInExam: 0,
+ percentCorrect: 0,
+ passingPercent: 0,
+ passed: false
+ },
+ isOpen,
+ closeModal
+}: ExamResultsModalProps): JSX.Element => {
+ const { t } = useTranslation();
+
+ if (!examResults) return <>>;
+
+ const {
+ numberOfCorrectAnswers,
+ examTimeInSeconds,
+ numberOfQuestionsInExam,
+ percentCorrect
+ } = examResults;
+
+ return (
+ {
+ closeModal('examResults');
+ }}
+ show={isOpen}
+ size='lg'
+ >
+
+
+ {t('settings.labels.results-for', { projectTitle })}
+
+
+
+
+
+ {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) })}
+
+
+
+
+
+
+
+ );
+};
+
+ExamResultsModal.displayName = 'ExamResultsModal';
+
+export default connect(mapStateToProps, mapDispatchToProps)(ExamResultsModal);
diff --git a/client/src/components/profile/components/time-line.tsx b/client/src/components/profile/components/time-line.tsx
index a51532fdc00..057ed35574b 100644
--- a/client/src/components/profile/components/time-line.tsx
+++ b/client/src/components/profile/components/time-line.tsx
@@ -14,6 +14,7 @@ import { regeneratePathAndHistory } from '../../../../../utils/polyvinyl';
import CertificationIcon from '../../../assets/icons/certification';
import { CompletedChallenge } from '../../../redux/prop-types';
import ProjectPreviewModal from '../../../templates/Challenges/components/project-preview-modal';
+import ExamResultsModal from '../../SolutionViewer/exam-results-modal';
import { openModal } from '../../../templates/Challenges/redux/actions';
import { Link, FullWidthRow } from '../../helpers';
import { SolutionDisplayWidget } from '../../solution-display-widget';
@@ -79,6 +80,14 @@ function TimelineInner({
openModal('projectPreview');
}
+ function viewExamResults(completedChallenge: CompletedChallenge): void {
+ setCompletedChallenge(completedChallenge);
+ setProjectTitle(
+ idToNameMap.get(completedChallenge.id)?.challengeTitle ?? ''
+ );
+ openModal('examResults');
+ }
+
function closeSolution(): void {
setSolutionOpen(false);
setCompletedChallenge(null);
@@ -108,6 +117,7 @@ function TimelineInner({
projectTitle={projectTitle}
showUserCode={() => viewSolution(completedChallenge)}
showProjectPreview={() => viewProject(completedChallenge)}
+ showExamResults={() => viewExamResults(completedChallenge)}
displayContext='timeline'
>
);
@@ -223,6 +233,10 @@ function TimelineInner({
previewTitle={projectTitle}
showProjectPreview={true}
/>
+
);
}
diff --git a/client/src/components/settings/certification.tsx b/client/src/components/settings/certification.tsx
index f379559a8be..a76d5e8c28b 100644
--- a/client/src/components/settings/certification.tsx
+++ b/client/src/components/settings/certification.tsx
@@ -10,6 +10,7 @@ import { connect } from 'react-redux';
import { regeneratePathAndHistory } from '../../../../utils/polyvinyl';
import ProjectPreviewModal from '../../templates/Challenges/components/project-preview-modal';
+import ExamResultsModal from '../SolutionViewer/exam-results-modal';
import { openModal } from '../../templates/Challenges/redux/actions';
import {
certTitles,
@@ -31,6 +32,7 @@ import './certification.css';
import {
ClaimedCertifications,
CompletedChallenge,
+ GeneratedExamResults,
User
} from '../../redux/prop-types';
import { createFlashMessage } from '../Flash/redux';
@@ -247,11 +249,13 @@ function CertificationSettings(props: CertificationSettingsProps) {
null
);
const [solution, setSolution] = useState();
+ const [examResults, setExamResults] = useState();
const [isOpen, setIsOpen] = useState(false);
function initialiseState() {
setProjectTitle('');
setChallengeFiles(null);
setSolution(null);
+ setExamResults(null);
setIsOpen(false);
}
@@ -268,8 +272,7 @@ function CertificationSettings(props: CertificationSettingsProps) {
if (!completedProject) {
return null;
}
-
- const { solution, challengeFiles } = completedProject;
+ const { solution, challengeFiles, examResults } = completedProject;
const showUserCode = () => {
setProjectTitle(projectTitle);
setChallengeFiles(challengeFiles);
@@ -293,11 +296,18 @@ function CertificationSettings(props: CertificationSettingsProps) {
openModal('projectPreview');
};
+ const showExamResults = () => {
+ setProjectTitle(projectTitle);
+ setExamResults(examResults as GeneratedExamResults);
+ openModal('examResults');
+ };
+
return (
+
);
diff --git a/client/src/components/solution-display-widget/index.tsx b/client/src/components/solution-display-widget/index.tsx
index f141e8de04e..416c21111ee 100644
--- a/client/src/components/solution-display-widget/index.tsx
+++ b/client/src/components/solution-display-widget/index.tsx
@@ -14,6 +14,7 @@ interface Props {
projectTitle: string;
showUserCode: () => void;
showProjectPreview?: () => void;
+ showExamResults?: () => void;
displayContext: 'timeline' | 'settings' | 'certification';
}
@@ -23,6 +24,7 @@ export function SolutionDisplayWidget({
projectTitle,
showUserCode,
showProjectPreview,
+ showExamResults,
displayContext
}: Props): JSX.Element | null {
const { id, solution, githubLink } = completedChallenge;
@@ -178,6 +180,20 @@ export function SolutionDisplayWidget({
);
+ const ShowExamResults = (
+
+ );
const MissingSolutionComponent =
displayContext === 'settings' ? (
<>{t('certification.project.no-solution')}>
@@ -190,6 +206,7 @@ export function SolutionDisplayWidget({
showMultifileProjectSolution: ShowMultifileProjectSolution,
showProjectAndGithubLinks: ShowProjectAndGithubLinkForCertification,
showProjectLink: ShowProjectLinkForCertification,
+ showExamResults: ShowExamResults,
none: MissingSolutionComponentForCertification
}
: {
@@ -197,6 +214,7 @@ export function SolutionDisplayWidget({
showMultifileProjectSolution: ShowMultifileProjectSolution,
showProjectAndGithubLinks: ShowProjectAndGithubLinks,
showProjectLink: ShowProjectLink,
+ showExamResults: ShowExamResults,
none: MissingSolutionComponent
};
diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts
index 527c6d64723..b350100b3a4 100644
--- a/client/src/redux/prop-types.ts
+++ b/client/src/redux/prop-types.ts
@@ -292,6 +292,7 @@ export type CompletedChallenge = {
challengeFiles:
| Pick[]
| null;
+ examResults?: GeneratedExamResults;
};
export type Ext = 'js' | 'html' | 'css' | 'jsx';
diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js
index d2b0025bf14..7471b0226e3 100644
--- a/client/src/templates/Challenges/redux/index.js
+++ b/client/src/templates/Challenges/redux/index.js
@@ -41,6 +41,7 @@ const initialState = {
reset: false,
exitExam: false,
finishExam: false,
+ examResults: false,
projectPreview: false,
shortcuts: false
},
diff --git a/client/src/templates/Challenges/redux/selectors.js b/client/src/templates/Challenges/redux/selectors.js
index b476e1ba0f5..0369b90802d 100644
--- a/client/src/templates/Challenges/redux/selectors.js
+++ b/client/src/templates/Challenges/redux/selectors.js
@@ -31,6 +31,8 @@ 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 isExamResultsModalOpenSelector = state =>
+ state[ns].modal.examResults;
export const isProjectPreviewModalOpenSelector = state =>
state[ns].modal.projectPreview;
export const isShortcutsModalOpenSelector = state => state[ns].modal.shortcuts;
diff --git a/client/src/utils/solution-display-type.ts b/client/src/utils/solution-display-type.ts
index 563afb4a0e8..543642e02dc 100644
--- a/client/src/utils/solution-display-type.ts
+++ b/client/src/utils/solution-display-type.ts
@@ -8,14 +8,17 @@ type DisplayType =
| 'showMultifileProjectSolution'
| 'showUserCode'
| 'showProjectAndGithubLinks'
- | 'showProjectLink';
+ | 'showProjectLink'
+ | 'showExamResults';
export const getSolutionDisplayType = ({
solution,
githubLink,
challengeFiles,
- challengeType
+ challengeType,
+ examResults
}: CompletedChallenge): DisplayType => {
+ if (examResults) return 'showExamResults';
if (challengeFiles?.length)
return challengeType === challengeTypes.multifileCertProject
? 'showMultifileProjectSolution'
diff --git a/cypress/e2e/default/learn/challenges/projects.ts b/cypress/e2e/default/learn/challenges/projects.ts
index 4a7c1863286..aaabeb5edac 100644
--- a/cypress/e2e/default/learn/challenges/projects.ts
+++ b/cypress/e2e/default/learn/challenges/projects.ts
@@ -154,9 +154,13 @@ describe('project submission', () => {
cy.setPrivacyTogglesToPublic();
cy.get(
- `a[href="/certification/developmentuser/${projectsInOrder[0]?.superBlock}"]`
+ '[data-cy="btn-for-javascript-algorithms-and-data-structures"]'
).click();
- cy.contains('Show Certification').click();
+ cy.get(
+ '[data-cy="btn-for-javascript-algorithms-and-data-structures"]'
+ )
+ .should('contain.text', 'Show Certification')
+ .click();
projectTitles.forEach(title => {
cy.get(`[data-cy="${title} solution"]`).click();