AA-196 course celebration cert (#197)
* AA-196 course celebration cert * AA-196: Course Celebration for passing Verified Learners Co-authored-by: Dillon Dumesnil <ddumesnil@edx.org> Note: This PR is being merged in somewhat incomplete as we decided to split off the work into a couple of other tickets. For example, the UI styling is not complete and I plan to also take another look at the routing. These code paths are not in use yet as the `courseExitPageIsActive` will always be False.
This commit is contained in:
181
src/courseware/course/course-exit/CourseCelebration.jsx
Normal file
181
src/courseware/course/course-exit/CourseCelebration.jsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Button, Hyperlink } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import celebration from './assets/celebration_456x328.gif';
|
||||
import certificate from './assets/certificate.png';
|
||||
import messages from './messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import { requestCert } from '../../../course-home/data/thunks';
|
||||
|
||||
function CourseCelebration({ intl }) {
|
||||
const { courseId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
certificateData,
|
||||
end,
|
||||
verifyIdentityUrl,
|
||||
} = useModel('courses', courseId);
|
||||
|
||||
const {
|
||||
certStatus,
|
||||
certWebViewUrl,
|
||||
downloadUrl,
|
||||
} = certificateData || { downloadUrl: 'google.com', certWebViewUrl: 'duckduckgo.com' };
|
||||
|
||||
const { username } = getAuthenticatedUser();
|
||||
|
||||
const dashboardLink = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getConfig().LMS_BASE_URL}/dashboard`}
|
||||
>
|
||||
{intl.formatMessage(messages.dashboardLink)}
|
||||
</Hyperlink>
|
||||
);
|
||||
const idVerificationSupportLink = getConfig().SUPPORT_URL && (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getConfig().SUPPORT_URL}/hc/en-us/articles/206503858-How-do-I-verify-my-identity`}
|
||||
>
|
||||
{intl.formatMessage(messages.idVerificationSupportLink)}
|
||||
</Hyperlink>
|
||||
);
|
||||
const profileLink = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getConfig().LMS_BASE_URL}/u/${username}`}
|
||||
>
|
||||
{intl.formatMessage(messages.profileLink)}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
let buttonLocation;
|
||||
let buttonText;
|
||||
let message;
|
||||
let title;
|
||||
// These cases are taken from the edx-platform `get_cert_data` function found in lms/courseware/views/views.py
|
||||
switch (certStatus) {
|
||||
case 'downloadable':
|
||||
title = intl.formatMessage(messages.certificateHeaderDownloadable);
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id="courseCelebration.certificateBody.available"
|
||||
defaultMessage="
|
||||
Showcase your accomplishment on LinkedIn or your resumé today.
|
||||
You can download your certificate now and access it any time from your
|
||||
{dashboardLink} and {profileLink}."
|
||||
description="Body in certificate banner"
|
||||
values={{ dashboardLink, profileLink }}
|
||||
/>
|
||||
);
|
||||
if (certWebViewUrl) {
|
||||
buttonLocation = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`;
|
||||
buttonText = intl.formatMessage(messages.viewCertificateButton);
|
||||
} else if (downloadUrl) {
|
||||
buttonLocation = downloadUrl;
|
||||
buttonText = intl.formatMessage(messages.downloadButton);
|
||||
}
|
||||
break;
|
||||
case 'earned_but_not_available': {
|
||||
const endDate = <FormattedDate value={end} day="numeric" month="long" year="numeric" />;
|
||||
title = intl.formatMessage(messages.certificateHeaderNotAvailable);
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id="courseCelebration.certificateBody.notAvailable"
|
||||
defaultMessage="
|
||||
After this course officially ends on {endDate}, you will receive an
|
||||
email notification with your certificate. Once you have your certificate,
|
||||
be sure to showcase your accomplishment on LinkedIn or your resumé.
|
||||
You will be able to access your certificate any time from your
|
||||
{dashboardLink} and {profileLink}."
|
||||
description="Body in certificate banner"
|
||||
values={{ endDate, dashboardLink, profileLink }}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'requesting':
|
||||
buttonText = intl.formatMessage(messages.requestCertificateButton);
|
||||
title = intl.formatMessage(messages.certificateHeaderRequestable);
|
||||
message = intl.formatMessage(messages.requestCertificateBodyText);
|
||||
break;
|
||||
case 'unverified':
|
||||
buttonText = intl.formatMessage(messages.verifyIdentityButton);
|
||||
buttonLocation = verifyIdentityUrl;
|
||||
title = intl.formatMessage(messages.certificateHeaderUnverified);
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id="courseCelebration.certificateBody.unverified"
|
||||
defaultMessage="In order to generate a certificate, you must complete ID verification.
|
||||
{idVerificationSupportLink} now."
|
||||
description="Body in certificate banner"
|
||||
values={{ idVerificationSupportLink }}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{`Congratulations! | ${getConfig().SITE_NAME}`}</title>
|
||||
</Helmet>
|
||||
<div className="align-items-center d-flex flex-column border border-light p-3 mb-3 mt-5">
|
||||
<h2>{ intl.formatMessage(messages.congratulationsHeader) }</h2>
|
||||
<div className="mb-3 h4 font-weight-normal">{ intl.formatMessage(messages.shareHeader) }</div>
|
||||
<div className="mb-5"><img src={celebration} alt="" className="img-fluid" /></div>
|
||||
<div className="d-flex flex-row p-4 mb-3 bg-primary-100">
|
||||
<div className="d-flex flex-column justify-content-between">
|
||||
<div className="h4 mb-0">{title}</div>
|
||||
{message}
|
||||
{/* The requesting status needs a different button because it does a POST instead of a GET */}
|
||||
{certStatus === 'requesting' ? (
|
||||
<div>
|
||||
<Button variant="outline-primary" onClick={() => dispatch(requestCert(courseId))} style={{ backgroundColor: '#FFFFFF' }}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Button variant="outline-primary" href={buttonLocation} style={{ backgroundColor: '#FFFFFF' }}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{certStatus !== 'unverified' && (
|
||||
<div className="col-3"><img src={certificate} alt="" className="img-fluid" /></div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-3 text-gray-500">
|
||||
<FontAwesomeIcon icon={faCalendarAlt} className="mr-2" style={{ width: '20px' }} />
|
||||
<FormattedMessage
|
||||
id="courseCelebration.dashboardInfo"
|
||||
defaultMessage="You can always access this course and its materials on your {dashboardLink}."
|
||||
description="Text letting the user know they can view their dashboard"
|
||||
values={{ dashboardLink }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CourseCelebration.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseCelebration);
|
||||
38
src/courseware/course/course-exit/CourseExit.jsx
Normal file
38
src/courseware/course/course-exit/CourseExit.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Redirect, useParams } from 'react-router-dom';
|
||||
|
||||
import CourseCelebration from './CourseCelebration';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
// These are taken from the edx-platform `get_cert_data` function found in lms/courseware/views/views.py
|
||||
const CELEBRATION_STATUSES = [
|
||||
'downloadable',
|
||||
'earned_but_not_available',
|
||||
'requesting',
|
||||
'unverified',
|
||||
];
|
||||
|
||||
export default function CourseExit() {
|
||||
const { courseId } = useParams();
|
||||
const {
|
||||
courseExitPageIsActive,
|
||||
userHasPassingGrade,
|
||||
certificateData,
|
||||
} = useModel('courses', courseId);
|
||||
|
||||
// userHasPassingGrade can be removed once there is an experience for failing learners
|
||||
if (!courseExitPageIsActive || !userHasPassingGrade) {
|
||||
return (<Redirect to={`/course/${courseId}`} />);
|
||||
}
|
||||
|
||||
const {
|
||||
certStatus,
|
||||
} = certificateData;
|
||||
|
||||
if (CELEBRATION_STATUSES.indexOf(certStatus) !== -1) {
|
||||
return (<CourseCelebration />);
|
||||
}
|
||||
// Just to be safe
|
||||
return (<Redirect to={`/course/${courseId}`} />);
|
||||
}
|
||||
BIN
src/courseware/course/course-exit/assets/celebration_456x328.gif
Normal file
BIN
src/courseware/course/course-exit/assets/celebration_456x328.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 469 KiB |
BIN
src/courseware/course/course-exit/assets/certificate.png
Normal file
BIN
src/courseware/course/course-exit/assets/certificate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
76
src/courseware/course/course-exit/messages.js
Normal file
76
src/courseware/course/course-exit/messages.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
certificateHeaderDownloadable: {
|
||||
id: 'courseCelebration.certificateHeader.downloadable',
|
||||
defaultMessage: 'Your certificate is available!',
|
||||
description: 'Text displayed when course certificate is ready to be downloaded',
|
||||
},
|
||||
certificateHeaderNotAvailable: {
|
||||
id: 'courseCelebration.certificateHeader.notAvailable',
|
||||
defaultMessage: 'Your certificate will be available soon!',
|
||||
description: 'Text displayed when course certificate is not yet available to view',
|
||||
},
|
||||
certificateHeaderUnverified: {
|
||||
id: 'courseCelebration.certificateHeader.unverified',
|
||||
defaultMessage: 'You must complete verification to receive your certificate.',
|
||||
description: 'Text displayed when a user has not verified their identity and cannot view their course certificate',
|
||||
},
|
||||
certificateHeaderRequestable: {
|
||||
id: 'courseCelebration.certificateHeader.requestable',
|
||||
defaultMessage: 'Congratulations, you qualified for a certificate!',
|
||||
description: 'Text displayed when a user has completed the course and can request a certificate',
|
||||
},
|
||||
congratulationsHeader: {
|
||||
id: 'courseCelebration.congratulationsHeader',
|
||||
defaultMessage: 'Congratulations!',
|
||||
description: 'Congratulations header on course completion page',
|
||||
},
|
||||
dashboardLink: {
|
||||
id: 'courseExit.dashboardLink',
|
||||
defaultMessage: 'Dashboard',
|
||||
description: "Link to learner's dashboard",
|
||||
},
|
||||
downloadButton: {
|
||||
id: 'courseCelebration.downloadButton',
|
||||
defaultMessage: 'Download my certificate',
|
||||
description: 'Text for download button in certificate banner',
|
||||
},
|
||||
idVerificationSupportLink: {
|
||||
id: 'courseExit.idVerificationSupportLink',
|
||||
defaultMessage: 'Learn more about ID verification',
|
||||
description: 'Link to help article about ID verification',
|
||||
},
|
||||
profileLink: {
|
||||
id: 'courseExit.profileLink',
|
||||
defaultMessage: 'Profile',
|
||||
description: 'Link to profile',
|
||||
},
|
||||
requestCertificateBodyText: {
|
||||
id: 'courseCelebration.requestCertificateBodyText',
|
||||
defaultMessage: 'In order to access your certificate, request it below.',
|
||||
description: 'Body text for certificate banner',
|
||||
},
|
||||
requestCertificateButton: {
|
||||
id: 'courseCelebration.requestCertificateButton',
|
||||
defaultMessage: 'Request certificate',
|
||||
description: 'Text for request certificate button in certificate banner',
|
||||
},
|
||||
shareHeader: {
|
||||
id: 'courseCelebration.shareHeader',
|
||||
defaultMessage: 'You have completed your course. Share your success on social media or email.',
|
||||
description: 'Social media/email share header',
|
||||
},
|
||||
verifyIdentityButton: {
|
||||
id: 'courseCelebration.verifyIdentityButton',
|
||||
defaultMessage: 'Verify now',
|
||||
description: 'Text for verify identity button in certificate banner',
|
||||
},
|
||||
viewCertificateButton: {
|
||||
id: 'courseCelebration.viewCertificateButton',
|
||||
defaultMessage: 'View now',
|
||||
description: 'Text for view certificate button in certificate banner',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
|
||||
import PageLoading from '../../../generic/PageLoading';
|
||||
import { UserMessagesContext, ALERT_TYPES } from '../../../generic/user-messages';
|
||||
@@ -113,6 +114,9 @@ function Sequence({
|
||||
}
|
||||
|
||||
const gated = sequence && sequence.gatedContent !== undefined && sequence.gatedContent.gated;
|
||||
const goToCourseExitPage = () => {
|
||||
history.push(`/course/${courseId}/course-exit`);
|
||||
};
|
||||
|
||||
if (sequenceStatus === 'loaded') {
|
||||
return (
|
||||
@@ -134,6 +138,7 @@ function Sequence({
|
||||
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
|
||||
handlePrevious();
|
||||
}}
|
||||
goToCourseExitPage={() => goToCourseExitPage()}
|
||||
/>
|
||||
<div className="unit-container flex-grow-1">
|
||||
<SequenceContent
|
||||
@@ -155,6 +160,7 @@ function Sequence({
|
||||
logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
|
||||
handleNext();
|
||||
}}
|
||||
goToCourseExitPage={() => goToCourseExitPage()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -20,13 +20,21 @@ export default function SequenceNavigation({
|
||||
onNavigate,
|
||||
nextSequenceHandler,
|
||||
previousSequenceHandler,
|
||||
goToCourseExitPage,
|
||||
}) {
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const { isFirstUnit, isLastUnit } = useSequenceNavigationMetadata(sequenceId, unitId);
|
||||
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
||||
const {
|
||||
courseId,
|
||||
sequenceStatus,
|
||||
} = useSelector(state => state.courseware);
|
||||
const isLocked = sequenceStatus === LOADED ? (
|
||||
sequence.gatedContent !== undefined && sequence.gatedContent.gated
|
||||
) : undefined;
|
||||
const {
|
||||
courseExitPageIsActive,
|
||||
userHasPassingGrade,
|
||||
} = useModel('courses', courseId);
|
||||
|
||||
const renderUnitButtons = () => {
|
||||
if (isLocked) {
|
||||
@@ -49,6 +57,52 @@ export default function SequenceNavigation({
|
||||
);
|
||||
};
|
||||
|
||||
const renderNextButton = () => {
|
||||
// AA-198: The userHasPassingGrade condition can be removed once we have a view for learners with failing grades
|
||||
const buttonOnClick = (isLastUnit && courseExitPageIsActive && userHasPassingGrade
|
||||
? goToCourseExitPage : nextSequenceHandler);
|
||||
// AA-198: The userHasPassingGrade condition can be removed once we have a view for learners with failing grades
|
||||
const disabled = isLastUnit && (!courseExitPageIsActive || !userHasPassingGrade);
|
||||
|
||||
let buttonText = (
|
||||
<FormattedMessage
|
||||
defaultMessage="Next"
|
||||
id="learn.sequence.navigation.next.button"
|
||||
description="The Next button in the sequence nav"
|
||||
/>
|
||||
);
|
||||
if (isLastUnit && courseExitPageIsActive && userHasPassingGrade) {
|
||||
buttonText = (
|
||||
<FormattedMessage
|
||||
defaultMessage="Complete the course"
|
||||
id="learn.sequence.navigation.completeCourse.button"
|
||||
description="The 'Complete the course' button in the sequence nav"
|
||||
/>
|
||||
);
|
||||
}
|
||||
// AA-198: Uncomment once there is a view for learners with failing grades
|
||||
// else if (isLastUnit && courseExitPageIsActive && !userHasPassingGrade) {
|
||||
// buttonText = (
|
||||
// <FormattedMessage
|
||||
// defaultMessage="Next"
|
||||
// id="learn.sequence.navigation.endOfCourse.button.next"
|
||||
// description="The 'next' text in the end of course button in the sequence nav"
|
||||
// />
|
||||
// <FormattedMessage
|
||||
// defaultMessage="(end of course)"
|
||||
// id="learn.sequence.navigation.endOfCourse.button.endOfCourse"
|
||||
// description="The '(end of course)' text in the end of course button in the sequence nav"
|
||||
// />
|
||||
// )
|
||||
// }
|
||||
return (
|
||||
<Button variant="link" className="next-btn" onClick={buttonOnClick} disabled={disabled}>
|
||||
{buttonText}
|
||||
<FontAwesomeIcon icon={faChevronRight} className="ml-2" size="sm" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return sequenceStatus === LOADED && (
|
||||
<nav className={classNames('sequence-navigation', className)}>
|
||||
<Button variant="link" className="previous-btn" onClick={previousSequenceHandler} disabled={isFirstUnit}>
|
||||
@@ -60,14 +114,7 @@ export default function SequenceNavigation({
|
||||
/>
|
||||
</Button>
|
||||
{renderUnitButtons()}
|
||||
<Button variant="link" className="next-btn" onClick={nextSequenceHandler} disabled={isLastUnit}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Next"
|
||||
id="learn.sequence.navigation.next.button"
|
||||
description="The Next button in the sequence nav"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faChevronRight} className="ml-2" size="sm" />
|
||||
</Button>
|
||||
{renderNextButton()}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -79,6 +126,7 @@ SequenceNavigation.propTypes = {
|
||||
onNavigate: PropTypes.func.isRequired,
|
||||
nextSequenceHandler: PropTypes.func.isRequired,
|
||||
previousSequenceHandler: PropTypes.func.isRequired,
|
||||
goToCourseExitPage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SequenceNavigation.defaultProps = {
|
||||
|
||||
@@ -4,7 +4,9 @@ import { Button } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSequenceNavigationMetadata } from './hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
export default function UnitNavigation(props) {
|
||||
const {
|
||||
@@ -12,9 +14,77 @@ export default function UnitNavigation(props) {
|
||||
unitId,
|
||||
onClickPrevious,
|
||||
onClickNext,
|
||||
goToCourseExitPage,
|
||||
} = props;
|
||||
|
||||
const { isFirstUnit, isLastUnit } = useSequenceNavigationMetadata(sequenceId, unitId);
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const {
|
||||
courseExitPageIsActive,
|
||||
userHasPassingGrade,
|
||||
} = useModel('courses', courseId);
|
||||
|
||||
const renderNextButton = () => {
|
||||
// AA-198: The userHasPassingGrade condition can be removed once we have a view for learners with failing grades
|
||||
const buttonOnClick = (isLastUnit && courseExitPageIsActive && userHasPassingGrade
|
||||
? goToCourseExitPage : onClickNext);
|
||||
// AA-198: The userHasPassingGrade condition can be removed once we have a view for learners with failing grades
|
||||
const disabled = isLastUnit && (!courseExitPageIsActive || !userHasPassingGrade);
|
||||
|
||||
// This is just to support what used to show while we are getting courseExitPageIsActive turned on.
|
||||
// This should be good to remove once disabled goes away.
|
||||
if (disabled) {
|
||||
return (
|
||||
<div className="m-2">
|
||||
<span role="img" aria-hidden="true">🤗</span> {/* This is a hugging face emoji */}
|
||||
{' '}
|
||||
<FormattedMessage
|
||||
id="learn.end.of.course"
|
||||
description="Message shown to students in place of a 'Next' button when they're at the end of a course."
|
||||
defaultMessage="You've reached the end of this course!"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let buttonText = (
|
||||
<FormattedMessage
|
||||
id="learn.sequence.navigation.after.unit.next"
|
||||
description="The button to go to the next unit"
|
||||
defaultMessage="Next"
|
||||
/>
|
||||
);
|
||||
if (isLastUnit && courseExitPageIsActive && userHasPassingGrade) {
|
||||
buttonText = (
|
||||
<FormattedMessage
|
||||
defaultMessage="Complete the course"
|
||||
id="learn.sequence.navigation.after.unit.completeCourse"
|
||||
description="The 'Complete the course' button in the unit nav"
|
||||
/>
|
||||
);
|
||||
}
|
||||
// AA-198: Uncomment once there is a view for learners with failing grades
|
||||
// else if (isLastUnit && courseExitPageIsActive && !userHasPassingGrade) {
|
||||
// buttonText = (
|
||||
// <FormattedMessage
|
||||
// defaultMessage="Next"
|
||||
// id="learn.sequence.navigation.endOfCourse.button.next"
|
||||
// description="The 'next' text in the end of course button in the sequence nav"
|
||||
// />
|
||||
// <FormattedMessage
|
||||
// defaultMessage="(end of course)"
|
||||
// id="learn.sequence.navigation.endOfCourse.button.endOfCourse"
|
||||
// description="The '(end of course)' text in the end of course button in the sequence nav"
|
||||
// />
|
||||
// )
|
||||
// }
|
||||
return (
|
||||
<Button variant="outline-primary" className="next-button" onClick={buttonOnClick} disabled={disabled}>
|
||||
{buttonText}
|
||||
<FontAwesomeIcon icon={faChevronRight} className="ml-2" size="sm" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="unit-navigation d-flex">
|
||||
@@ -31,31 +101,7 @@ export default function UnitNavigation(props) {
|
||||
defaultMessage="Previous"
|
||||
/>
|
||||
</Button>
|
||||
{isLastUnit ? (
|
||||
<div className="m-2">
|
||||
<span role="img" aria-hidden="true">🤗</span> {/* This is a hugging face emoji */}
|
||||
{' '}
|
||||
<FormattedMessage
|
||||
id="learn.end.of.course"
|
||||
description="Message shown to students in place of a 'Next' button when they're at the end of a course."
|
||||
defaultMessage="You've reached the end of this course!"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
className="next-button"
|
||||
onClick={onClickNext}
|
||||
disabled={isLastUnit}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="learn.sequence.navigation.after.unit.next"
|
||||
description="The button to go to the next unit"
|
||||
defaultMessage="Next"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faChevronRight} className="ml-2" size="sm" />
|
||||
</Button>
|
||||
)}
|
||||
{renderNextButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -65,6 +111,7 @@ UnitNavigation.propTypes = {
|
||||
unitId: PropTypes.string,
|
||||
onClickPrevious: PropTypes.func.isRequired,
|
||||
onClickNext: PropTypes.func.isRequired,
|
||||
goToCourseExitPage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
UnitNavigation.defaultProps = {
|
||||
|
||||
@@ -135,6 +135,10 @@ function normalizeMetadata(metadata) {
|
||||
notes: camelCaseObject(metadata.notes),
|
||||
marketingUrl: metadata.marketing_url,
|
||||
celebrations: camelCaseObject(metadata.celebrations),
|
||||
userHasPassingGrade: metadata.user_has_passing_grade,
|
||||
courseExitPageIsActive: metadata.course_exit_page_is_active,
|
||||
certificateData: camelCaseObject(metadata.certificate_data),
|
||||
verifyIdentityUrl: metadata.verify_identity_url,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { UserMessagesProvider } from './generic/user-messages';
|
||||
import './index.scss';
|
||||
import './assets/favicon.ico';
|
||||
import OutlineTab from './course-home/outline-tab';
|
||||
import CourseExit from './courseware/course/course-exit/CourseExit';
|
||||
import CoursewareContainer from './courseware';
|
||||
import CoursewareRedirectLandingPage from './courseware/CoursewareRedirectLandingPage';
|
||||
import DatesTab from './course-home/dates-tab';
|
||||
@@ -26,6 +27,7 @@ import ProgressTab from './course-home/progress-tab/ProgressTab';
|
||||
import { TabContainer } from './tab-page';
|
||||
|
||||
import { fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/data';
|
||||
import { fetchCourse } from './courseware/data';
|
||||
import initializeStore from './store';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
@@ -49,6 +51,11 @@ subscribe(APP_READY, () => {
|
||||
<ProgressTab />
|
||||
</TabContainer>
|
||||
</Route>
|
||||
<Route path="/course/:courseId/course-exit">
|
||||
<TabContainer tab="exit" fetch={fetchCourse} isExitPage>
|
||||
<CourseExit />
|
||||
</TabContainer>
|
||||
</Route>
|
||||
<Route
|
||||
path={[
|
||||
'/course/:courseId/:sequenceId/:unitId',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
@@ -36,6 +36,12 @@ function getStudioUrl(courseId, unitId) {
|
||||
}
|
||||
|
||||
export default function InstructorToolbar(props) {
|
||||
const [didMount, setDidMount] = useState(false);
|
||||
useEffect(() => {
|
||||
setDidMount(true);
|
||||
return () => setDidMount(false);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
courseId,
|
||||
unitId,
|
||||
@@ -51,6 +57,10 @@ export default function InstructorToolbar(props) {
|
||||
});
|
||||
const urlStudio = getStudioUrl(courseId, unitId);
|
||||
const [masqueradeErrorMessage, showMasqueradeError] = useState(null);
|
||||
|
||||
if (!didMount) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-primary text-white">
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
import messages from './messages';
|
||||
|
||||
class MasqueradeWidget extends Component {
|
||||
isMounted = false;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.courseId = props.courseId;
|
||||
@@ -29,8 +31,9 @@ class MasqueradeWidget extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.isMounted = true;
|
||||
getMasqueradeOptions(this.courseId).then((data) => {
|
||||
if (data.success) {
|
||||
if (data.success && this.isMounted) {
|
||||
this.onSuccess(data);
|
||||
} else {
|
||||
// This was explicitly denied by the backend;
|
||||
@@ -47,6 +50,10 @@ class MasqueradeWidget extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
onError(message) {
|
||||
this.props.onError(message);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export default function TabContainer(props) {
|
||||
children,
|
||||
fetch,
|
||||
tab,
|
||||
isExitPage,
|
||||
} = props;
|
||||
|
||||
const { courseId: courseIdFromUrl } = useParams();
|
||||
@@ -21,10 +22,11 @@ export default function TabContainer(props) {
|
||||
|
||||
// The courseId from the store is the course we HAVE loaded. If the URL changes,
|
||||
// we don't want the application to adjust to it until it has actually loaded the new data.
|
||||
const slice = isExitPage ? 'courseware' : 'courseHome';
|
||||
const {
|
||||
courseId,
|
||||
courseStatus,
|
||||
} = useSelector(state => state.courseHome);
|
||||
} = useSelector(state => state[slice]);
|
||||
|
||||
return (
|
||||
<TabPage
|
||||
@@ -40,5 +42,10 @@ export default function TabContainer(props) {
|
||||
TabContainer.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
fetch: PropTypes.func.isRequired,
|
||||
isExitPage: PropTypes.bool,
|
||||
tab: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
TabContainer.defaultProps = {
|
||||
isExitPage: false,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user