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();