Skip to content
5 changes: 3 additions & 2 deletions src/components/CompletionStatus.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ describe('CompletionStatus', () => {
props = {
numberOfQuestions: 15,
numberCompleted: 0,
handleClick: () => {console.log('click')}
handleNext: () => {console.log('next')},
handleContinue: () => {console.log('continue')}
}
});

Expand Down Expand Up @@ -37,7 +38,7 @@ describe('CompletionStatus', () => {
it('clicking triggers handler', () => {
const mockEv = jest.fn();
const component = renderer.create(
<CompletionStatus {...props} handleClick={mockEv} />
<CompletionStatus {...props} handleContinue={mockEv} />
);

renderer.act(() => {
Expand Down
16 changes: 15 additions & 1 deletion src/components/CompletionStatus.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,24 @@ import { CompletionStatus, CompletionStatusProps } from './CompletionStatus';
const props: CompletionStatusProps = {
numberOfQuestions: 15,
numberCompleted: 0,
handleClick: () => {console.log('click')}
handleNext: () => {console.log('next')},
handleContinue: () => {console.log('continue')},
};

const unlimitedProps: CompletionStatusProps = {
numberOfQuestions: 15,
numberCompleted: 0,
handleNext: () => {console.log('next')},
handleContinue: () => {console.log('continue')},
handleRetry: () => {console.log('retry')},
scoreSoFar: '5/15',
savedScore: '10/15'
}
export const Default = () => <CompletionStatus {...props} />;
export const PartialComplete = () => <CompletionStatus {...props} numberCompleted={3} />
export const Complete = () => <CompletionStatus {...props} numberCompleted={15} />
export const unlimitedComplete = () => <CompletionStatus {...unlimitedProps} numberCompleted={15}/>;
export const unlimitedPartial = () => <CompletionStatus {...unlimitedProps} numberCompleted={5} />
export const unlimitedCompletedNoScore = () => <CompletionStatus {...unlimitedProps} scoreSoFar={undefined} savedScore={undefined} numberCompleted={15}/>;
export const unlimitedPartialNoProgress = () => <CompletionStatus {...unlimitedProps} savedScore='0/15' scoreSoFar='0/15' numberCompleted={0}/>;

136 changes: 118 additions & 18 deletions src/components/CompletionStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import styled, { createGlobalStyle } from "styled-components";
import { InnerStepCard } from "./Card";
import Button from "./Button";

import { colors } from '../theme';
const GlobalStyle = createGlobalStyle`
:root {
--content-text-scale: 1;
Expand All @@ -11,8 +11,12 @@ const GlobalStyle = createGlobalStyle`
export interface CompletionStatusProps {
numberOfQuestions: number;
numberCompleted: number;
handleClick: () => void;
handleContinue: () => void;
handleNext: () => void;
className?: string;
scoreSoFar?: string;
savedScore?: string;
handleRetry?: () => void;
}

const CompletionStatusCard = styled(InnerStepCard)`
Expand All @@ -22,7 +26,6 @@ const CompletionStatusCard = styled(InnerStepCard)`
display: block;

button {
min-width: 160px;
height: 48px;
}

Expand All @@ -36,24 +39,121 @@ const CompletionHeader = styled.h2`
margin: 0;
`;

const ButtonGroup = styled.div`
display: flex;
margin: 0;
gap: 1rem;

button {
height: 48px;
}
`;

const ScoreGroup = styled.div`
display: flex;
margin: 0;
gap: 1rem;
`;

const RetryResumeButton = styled(Button)`
background-color: ${colors.palette.white};
color: ${colors.palette.black};
border: 1px solid ${colors.palette.pale};
font-weight: normal;
transition: background-color 0.2s ease;

&:hover {
background-color: ${colors.palette.neutralBright} !important;
color: ${colors.palette.black} !important;
border: 1px solid ${colors.palette.pale} !important;
}

&:active {
background-color: ${colors.palette.neutralLight} !important;
color: ${colors.palette.black} !important;
border: 1px solid ${colors.palette.pale} !important;
}
`;

export const CompletionStatus = styled(({
numberOfQuestions, numberCompleted, handleClick, className
numberOfQuestions,
numberCompleted,
handleContinue,
handleNext,
className,
scoreSoFar,
savedScore,
handleRetry
}: CompletionStatusProps) => {

const allCompleted = numberOfQuestions === numberCompleted;
const someCompleted = numberCompleted > 0;
const buttonText = allCompleted ? 'Next' : (
someCompleted ? 'Continue' : 'Start'
);

return <>
<GlobalStyle />
<CompletionStatusCard className={className}>
<CompletionHeader>{allCompleted ? 'You are done.' : (someCompleted ? 'Quiz is partially complete.' : 'No questions have been answered.')}</CompletionHeader>
<p>{allCompleted ? 'Great job answering all the questions.' : (someCompleted ? `You've completed ${numberCompleted} of ${numberOfQuestions} questions.` : 'Begin working on the quiz.')}</p>
<Button data-test-id={`${buttonText.split(' ')[0].toLowerCase()}-btn`} onClick={() => handleClick()}>
{buttonText}
</Button>
</CompletionStatusCard>
</>
const buttonText = allCompleted || (numberCompleted === 0 && handleRetry) || (someCompleted && handleRetry) ? 'Next' : (someCompleted ? 'Continue' : 'Start');

const retryOrResume = allCompleted ? 'Retry Quiz' : 'Resume Quiz';
const unlimitedDone = "Attempts for this quiz are unlimited. Your highest score will be saved.";
const unlimitedCurrent = "You are in the middle of a quiz attempt. Attempts for this quiz are unlimited. Your highest score will be saved.";

// When allCompleted, clicking Retry/Resume should create a new attempt (handleRetry)
// When not completed, clicking Retry/Resume should resume (handleContinue)
const onRetryResumeClick = allCompleted
? handleRetry
: handleContinue;

// if unlimited attempts (handleRetry) is active always show next button
// if all is completed show next button
// if not unlimited and incomplete show and handle continue
const onNextContinueClick = allCompleted || handleRetry
? handleNext
: handleContinue;

return (
<>
<GlobalStyle />
<CompletionStatusCard className={className}>
<CompletionHeader>
{allCompleted
? 'You are done.'
: (someCompleted ? 'Quiz is partially complete.' : 'No questions have been answered.')}
</CompletionHeader>

{handleRetry ? (
<div>
<p>{allCompleted ? unlimitedDone : unlimitedCurrent}</p>
<ScoreGroup>
<p>
<b>Current Score:</b> {scoreSoFar ?? 'Score unavailable'} | <b>Saved Score:</b> {savedScore ?? 'Score unavailable'}
</p>
</ScoreGroup>
</div>
) : (
<p>
{allCompleted
? 'Great job answering all the questions.'
: (someCompleted
? `You've completed ${numberCompleted} of ${numberOfQuestions} questions.`
: 'Begin working on the quiz.')}
</p>
)}

<ButtonGroup>
{handleRetry ? (
<RetryResumeButton
data-test-id="retry-resume-btn"
onClick={onRetryResumeClick}
>
{retryOrResume}
</RetryResumeButton>
) : null}

<Button
data-test-id={`${buttonText.split(' ')[0].toLowerCase()}-btn`}
onClick={onNextContinueClick}
>
{buttonText}
</Button>
</ButtonGroup>
</CompletionStatusCard>
</>
);
})``;
3 changes: 3 additions & 0 deletions src/components/Exercise.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('Exercise', () => {
},
questionNumber: 1,
hasMultipleAttempts: false,
hasUnlimitedAttempts: false,
onAnswerChange: () => null,
onAnswerSave: () => null,
onNextStep: () => null,
Expand Down Expand Up @@ -142,6 +143,7 @@ describe('Exercise', () => {
numberOfQuestions: 1,
scrollToQuestion: 1,
hasMultipleAttempts: false,
hasUnlimitedAttempts: false,
hasFeedback: true,
onAnswerChange: () => null,
onAnswerSave: () => null,
Expand Down Expand Up @@ -343,6 +345,7 @@ describe('Exercise', () => {
},
questionNumber: 1,
hasMultipleAttempts: false,
hasUnlimitedAttempts: false,
onAnswerChange: () => null,
onAnswerSave: () => null,
onNextStep: () => null,
Expand Down
3 changes: 3 additions & 0 deletions src/components/Exercise.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const exerciseWithStepDataProps: ExerciseWithStepDataProps = {
},
questionNumber: 1,
hasMultipleAttempts: false,
hasUnlimitedAttempts: false,
onAnswerChange: () => null,
onAnswerSave: () => null,
onNextStep: () => null,
Expand Down Expand Up @@ -116,6 +117,7 @@ const exerciseWithQuestionStatesProps = (uid?: string, correctness?: string): Ex
questionNumber: 1,
numberOfQuestions: 1,
hasMultipleAttempts: false,
hasUnlimitedAttempts: false,
onAnswerChange: () => null,
onAnswerSave: () => null,
onNextStep: () => null,
Expand Down Expand Up @@ -456,6 +458,7 @@ export const MultiPartHalfComplete = () => {
questionNumber: 1,
numberOfQuestions: 2,
hasMultipleAttempts: false,
hasUnlimitedAttempts: false,
onAnswerChange: () => null,
onAnswerSave: () => null,
onNextStep: () => null,
Expand Down
2 changes: 2 additions & 0 deletions src/components/Exercise/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ export interface ExerciseBaseProps {
/** A boolean that enables showing the amount of attempts remaining. */
hasMultipleAttempts: boolean;
/** A callback with the question_id when the Submit/Re-submit button is clicked. */
hasUnlimitedAttempts: boolean;
/** A boolean that enables labeling for unlimited attempts. */
onAnswerSave: (question_id: number) => void;
/** A callback with the current question index when the Next/Continue button is clicked. */
onNextStep: (currentIndex: number) => void;
Expand Down
1 change: 1 addition & 0 deletions src/components/ExercisePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const ExercisePreview = ({
canAnswer: true,
needsSaved: true,
hasMultipleAttempts: false,
hasUnlimitedAttempts: false,
onAnswerChange: () => undefined,
onAnswerSave: () => undefined,
onNextStep: () => undefined,
Expand Down
1 change: 1 addition & 0 deletions src/components/ExerciseQuestion.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('ExerciseQuestion', () => {
questionNumber: 1,
choicesEnabled: false,
hasMultipleAttempts: false,
hasUnlimitedAttempts: false,
onAnswerChange: () => null,
onAnswerSave: () => null,
onNextStep: () => null,
Expand Down
32 changes: 31 additions & 1 deletion src/components/ExerciseQuestion.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const props = {
onChange: () => null,
choicesEnabled: true,
hasMultipleAttempts: false,
hasUnlimitedAttempts: false,
onAnswerChange: () => null,
onAnswerSave: () => null,
onNextStep: () => null,
Expand All @@ -63,22 +64,51 @@ export const FreeResponseEntered = () =>
export const MultipleAttemptsAllLeft = () =>
<ExerciseQuestion {...props}
hasMultipleAttempts={true}
hasUnlimitedAttempts={false}
attempts_remaining={2}
attempt_number={0}
/>;
export const MultipleAttemptsOneLeft = () =>
<ExerciseQuestion {...props}
hasMultipleAttempts={true}
hasUnlimitedAttempts={false}
canAnswer={true}
needsSaved={false}
canUpdateCurrentStep={true}
attempts_remaining={1}
attempt_number={1}
incorrectAnswerId='2'
/>;
export const MultipleAttemptsNoneLeft = () =>

export const MultipleAttemptsOneLeftAndUnlimited = () =>
<ExerciseQuestion {...props}
hasMultipleAttempts={true}
hasUnlimitedAttempts={true}
canAnswer={true}
needsSaved={false}
canUpdateCurrentStep={true}
attempts_remaining={1}
attempt_number={1}
incorrectAnswerId='2'
/>;

export const MultipleAttemptsNoneLeftAndUnlimited = () =>
<ExerciseQuestion {...props}
hasMultipleAttempts={true}
hasUnlimitedAttempts={true}
choicesEnabled={false}
canAnswer={false}
needsSaved={false}
canUpdateCurrentStep={false}
attempts_remaining={0}
attempt_number={2}
incorrectAnswerId='2'
/>;

export const UnlimitedAttemptsOnly = () =>
<ExerciseQuestion {...props}
hasMultipleAttempts={false}
hasUnlimitedAttempts={true}
choicesEnabled={false}
canAnswer={false}
needsSaved={false}
Expand Down
12 changes: 11 additions & 1 deletion src/components/ExerciseQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface ExerciseQuestionProps {
questionNumber: number;
choicesEnabled: boolean;
hasMultipleAttempts: boolean;
hasUnlimitedAttempts: boolean;
onAnswerChange: () => void;
onAnswerSave: ExerciseBaseProps['onAnswerSave'];
onNextStep: ExerciseBaseProps['onNextStep'];
Expand Down Expand Up @@ -48,6 +49,12 @@ const AttemptsRemaining = ({ count }: { count: number }) => {
);
}

const UnlimitedAttempts = () => {
return (
<div>Unlimited quiz attempts left</div>
);
}

const PublishedComments = ({ published_comments }: { published_comments?: string }) => {
if (!published_comments) { return null; }

Expand Down Expand Up @@ -96,7 +103,7 @@ export const ExerciseQuestion = React.forwardRef((props: ExerciseQuestionProps,
const {
question, task, answer_id_order, onAnswerChange, feedback_html, correct_answer_feedback_html,
is_completed, correct_answer_id, incorrectAnswerId, choicesEnabled, questionNumber,
answer_id, hasMultipleAttempts, attempts_remaining, published_comments, detailedSolution,
answer_id, hasMultipleAttempts, hasUnlimitedAttempts, attempts_remaining, published_comments, detailedSolution,
canAnswer, needsSaved, attempt_number, apiIsPending, onAnswerSave, onNextStep, canUpdateCurrentStep,
displaySolution, available_points, free_response, labelAnswers, show_all_feedback, tableFeedbackEnabled,
hasFeedback, previewMode
Expand Down Expand Up @@ -145,7 +152,10 @@ export const ExerciseQuestion = React.forwardRef((props: ExerciseQuestionProps,
{hasMultipleAttempts &&
attempts_remaining > 0 &&
<AttemptsRemaining count={attempts_remaining} />}
{ hasUnlimitedAttempts ? <UnlimitedAttempts/>: null}

</span>

<PublishedComments published_comments={published_comments} />
{detailedSolution && (<div><strong>Detailed solution:</strong> <Content html={detailedSolution} /></div>)}
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/Print.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const Default = () => (
canAnswer={true}
needsSaved={true}
hasMultipleAttempts={false}
hasUnlimitedAttempts={false}
onAnswerChange={() => undefined}
onAnswerSave={() => undefined}
onNextStep={() => undefined}
Expand Down
Loading
Loading