AA-388: CourseExitPage UI improvements (#237)

This commit is contained in:
Carla Duarte
2020-10-14 10:34:55 -04:00
committed by GitHub
parent 1950fe56bd
commit e9cf5e58de
10 changed files with 211 additions and 126 deletions

View File

@@ -3,6 +3,7 @@ import React from 'react';
import {
FormattedDate, FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { layoutGenerator } from 'react-break';
import { Helmet } from 'react-helmet';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
@@ -12,13 +13,22 @@ 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 CelebrationMobile from './assets/celebration_456x328.gif';
import CelebrationDesktop from './assets/celebration_750x540.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 layout = layoutGenerator({
mobile: 0,
tablet: 768,
});
const OnMobile = layout.is('mobile');
const OnAtLeastTablet = layout.isAtLeast('tablet');
const { courseId } = useParams();
const dispatch = useDispatch();
const {
@@ -31,7 +41,7 @@ function CourseCelebration({ intl }) {
certStatus,
certWebViewUrl,
downloadUrl,
} = certificateData || { downloadUrl: 'google.com', certWebViewUrl: 'duckduckgo.com' };
} = certificateData;
const { username } = getAuthenticatedUser();
@@ -43,6 +53,7 @@ function CourseCelebration({ intl }) {
{intl.formatMessage(messages.dashboardLink)}
</Hyperlink>
);
// todo: remove this hardcoded link to edX support
const idVerificationSupportLink = getConfig().SUPPORT_URL && (
<Hyperlink
style={{ textDecoration: 'underline' }}
@@ -75,7 +86,6 @@ function CourseCelebration({ intl }) {
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 }}
/>
);
@@ -91,17 +101,31 @@ function CourseCelebration({ intl }) {
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 }}
/>
<>
<div className="mb-2">
<FormattedMessage
id="courseCelebration.certificateBody.notAvailable.endDate"
defaultMessage="After this course officially ends on {endDate}, you will receive an
email notification with your certificate."
values={{ endDate }}
/>
</div>
<div className="mb-2">
<FormattedMessage
id="courseCelebration.certificateBody.notAvailable.shareSuggestion"
defaultMessage="Once you have your certificate, be sure to showcase your accomplishment on
LinkedIn or your resumé."
/>
</div>
<div className="mb-2">
<FormattedMessage
id="courseCelebration.certificateBody.notAvailable.accessCertificate"
defaultMessage="You will be able to access your certificate any time from your
{dashboardLink} and {profileLink}."
values={{ dashboardLink, profileLink }}
/>
</div>
</>
);
break;
}
@@ -114,12 +138,12 @@ function CourseCelebration({ intl }) {
buttonText = intl.formatMessage(messages.verifyIdentityButton);
buttonLocation = verifyIdentityUrl;
title = intl.formatMessage(messages.certificateHeaderUnverified);
// todo: check for idVerificationSupportLink null
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 }}
/>
);
@@ -131,43 +155,78 @@ function CourseCelebration({ intl }) {
return (
<>
<Helmet>
<title>{`Congratulations! | ${getConfig().SITE_NAME}`}</title>
<title>{`${intl.formatMessage(messages.congratulationsHeader)} | ${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' }}>
<div className="row w-100 mx-0 my-4 px-5 py-3 border border-light">
<div className="col-12 p-0 h2 text-center">
{intl.formatMessage(messages.congratulationsHeader)}
</div>
<div className="col-12 p-0 font-weight-normal lead text-center">
{intl.formatMessage(messages.shareHeader)}
</div>
<div className="col-12 my-4 px-0 px-md-5 text-center">
<OnMobile>
<img
src={CelebrationMobile}
alt={`${intl.formatMessage(messages.congratulationsImage)}`}
className="img-fluid"
/>
</OnMobile>
<OnAtLeastTablet>
<img
src={CelebrationDesktop}
alt={`${intl.formatMessage(messages.congratulationsImage)}`}
className="img-fluid"
style={{ width: '36rem' }}
/>
</OnAtLeastTablet>
</div>
<div className="col-12 px-0 px-md-5">
<div className="row w-100 m-0 p-4 bg-primary-100">
<div className="col order-1 order-md-0 pl-0 pr-0 pr-md-5">
<div className="h4">{title}</div>
<p>{message}</p>
{/* The requesting status needs a different button because it does a POST instead of a GET */}
{certStatus === 'requesting' && (
<Button
variant="outline-primary"
onClick={() => dispatch(requestCert(courseId))}
style={{ backgroundColor: 'white' }}
>
{buttonText}
</Button>
</div>
) : (
<div>
<Button variant="outline-primary" href={buttonLocation} style={{ backgroundColor: '#FFFFFF' }}>
)}
{buttonLocation && (
<Button
variant="outline-primary"
href={buttonLocation}
style={{ backgroundColor: 'white' }}
>
{buttonText}
</Button>
)}
</div>
{certStatus !== 'unverified' && (
<div className="col-12 order-0 col-md-3 order-md-1 w-100 mb-3 p-0 text-center">
<img
src={certificate}
alt={`${intl.formatMessage(messages.certificateImage)}`}
className="w-100"
style={{ maxWidth: '13rem' }}
/>
</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 className="row w-100 mx-0 my-3 justify-content-center">
<p className="text-gray-700">
<FontAwesomeIcon icon={faCalendarAlt} style={{ width: '20px' }} />&nbsp;
<FormattedMessage
id="courseCelebration.dashboardInfo"
defaultMessage="You can always access this course and its materials on your {dashboardLink}."
values={{ dashboardLink }}
/>
</p>
</div>
</div>
</div>
</>

View File

@@ -1,8 +1,12 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { Redirect, useParams } from 'react-router-dom';
import CourseCelebration from './CourseCelebration';
import messages from './messages';
import { useModel } from '../../../generic/model-store';
// These are taken from the edx-platform `get_cert_data` function found in lms/courseware/views/views.py
@@ -13,7 +17,7 @@ const CELEBRATION_STATUSES = [
'unverified',
];
export default function CourseExit() {
function CourseExit({ intl }) {
const { courseId } = useParams();
const {
courseExitPageIsActive,
@@ -31,8 +35,26 @@ export default function CourseExit() {
} = certificateData;
if (CELEBRATION_STATUSES.indexOf(certStatus) !== -1) {
return (<CourseCelebration />);
return (
<>
<div className="row w-100 m-0 justify-content-end">
<Button
variant="outline-primary"
href={`${getConfig().LMS_BASE_URL}/dashboard`}
>
{intl.formatMessage(messages.viewCoursesButton)}
</Button>
</div>
<CourseCelebration />
</>
);
}
// Just to be safe
return (<Redirect to={`/course/${courseId}`} />);
}
CourseExit.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CourseExit);

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

View File

@@ -9,7 +9,7 @@ const messages = defineMessages({
certificateHeaderNotAvailable: {
id: 'courseCelebration.certificateHeader.notAvailable',
defaultMessage: 'Your certificate will be available soon!',
description: 'Text displayed when course certificate is not yet available to view',
description: 'Text displayed when course certificate is not yet available to be viewed',
},
certificateHeaderUnverified: {
id: 'courseCelebration.certificateHeader.unverified',
@@ -21,55 +21,67 @@ const messages = defineMessages({
defaultMessage: 'Congratulations, you qualified for a certificate!',
description: 'Text displayed when a user has completed the course and can request a certificate',
},
certificateImage: {
id: 'courseCelebration.certificateImage',
defaultMessage: 'Sample certificate',
description: 'Alt text used to describe an image of a certificate',
},
congratulationsHeader: {
id: 'courseCelebration.congratulationsHeader',
defaultMessage: 'Congratulations!',
description: 'Congratulations header on course completion page',
},
congratulationsImage: {
id: 'courseCelebration.congratulationsImage',
defaultMessage: 'Four people raising their hands in celebration',
description: 'Alt text used to describe celebratory image',
},
dashboardLink: {
id: 'courseExit.dashboardLink',
defaultMessage: 'Dashboard',
description: "Link to learner's dashboard",
description: "Link to user's dashboard",
},
downloadButton: {
id: 'courseCelebration.downloadButton',
defaultMessage: 'Download my certificate',
description: 'Text for download button in certificate banner',
description: 'Button to download the course certificate',
},
idVerificationSupportLink: {
id: 'courseExit.idVerificationSupportLink',
defaultMessage: 'Learn more about ID verification',
description: 'Link to help article about ID verification',
description: 'Link to an article about identity verification',
},
profileLink: {
id: 'courseExit.profileLink',
defaultMessage: 'Profile',
description: 'Link to profile',
description: "Link to user's 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',
description: 'Button to request the course certificate',
},
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',
defaultMessage: 'Verify ID now',
description: 'Button to verify the identify of the user',
},
viewCertificateButton: {
id: 'courseCelebration.viewCertificateButton',
defaultMessage: 'View now',
description: 'Text for view certificate button in certificate banner',
defaultMessage: 'View my certificate',
description: 'Button to view the course certificate',
},
viewCoursesButton: {
id: 'courseExit.viewCoursesButton',
defaultMessage: 'View my courses',
description: 'Button to redirect user to their course dashboard',
},
});

View File

@@ -4,7 +4,7 @@ import { Button } from '@edx/paragon';
import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
import UnitButton from './UnitButton';
@@ -13,7 +13,10 @@ import { useSequenceNavigationMetadata } from './hooks';
import { useModel } from '../../../../generic/model-store';
import { LOADED } from '../../../data/slice';
export default function SequenceNavigation({
import messages from './messages';
function SequenceNavigation({
intl,
unitId,
sequenceId,
className,
@@ -64,21 +67,9 @@ export default function SequenceNavigation({
// 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"
/>
);
let buttonText = (intl.formatMessage(messages.nextButton));
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"
/>
);
buttonText = (intl.formatMessage(messages.completeCourseButton));
}
// AA-198: Uncomment once there is a view for learners with failing grades
// else if (isLastUnit && courseExitPageIsActive && !userHasPassingGrade) {
@@ -107,11 +98,7 @@ export default function SequenceNavigation({
<nav className={classNames('sequence-navigation', className)}>
<Button variant="link" className="previous-btn" onClick={previousSequenceHandler} disabled={isFirstUnit}>
<FontAwesomeIcon icon={faChevronLeft} className="mr-2" size="sm" />
<FormattedMessage
defaultMessage="Previous"
id="learn.sequence.navigation.previous.button"
description="The Previous button in the sequence nav"
/>
{intl.formatMessage(messages.previousButton)}
</Button>
{renderUnitButtons()}
{renderNextButton()}
@@ -120,6 +107,7 @@ export default function SequenceNavigation({
}
SequenceNavigation.propTypes = {
intl: intlShape.isRequired,
sequenceId: PropTypes.string.isRequired,
unitId: PropTypes.string,
className: PropTypes.string,
@@ -133,3 +121,5 @@ SequenceNavigation.defaultProps = {
className: null,
unitId: null,
};
export default injectIntl(SequenceNavigation);

View File

@@ -28,6 +28,7 @@ describe('Sequence Navigation', () => {
previousSequenceHandler: () => {},
onNavigate: () => {},
nextSequenceHandler: () => {},
goToCourseExitPage: () => {},
};
});

View File

@@ -3,20 +3,21 @@ import PropTypes from 'prop-types';
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 { injectIntl, intlShape } 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 {
sequenceId,
unitId,
onClickPrevious,
onClickNext,
goToCourseExitPage,
} = props;
import messages from './messages';
function UnitNavigation({
intl,
sequenceId,
unitId,
onClickPrevious,
onClickNext,
goToCourseExitPage,
}) {
const { isFirstUnit, isLastUnit } = useSequenceNavigationMetadata(sequenceId, unitId);
const { courseId } = useSelector(state => state.courseware);
const {
@@ -38,45 +39,18 @@ export default function UnitNavigation(props) {
<div className="m-2">
<span role="img" aria-hidden="true">&#129303;</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!"
/>
{intl.formatMessage(messages.endOfCourse)}
</div>
);
}
let buttonText = (
<FormattedMessage
id="learn.sequence.navigation.after.unit.next"
description="The button to go to the next unit"
defaultMessage="Next"
/>
);
let buttonText = (intl.formatMessage(messages.nextButton));
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"
/>
);
buttonText = (intl.formatMessage(messages.completeCourseButton));
}
// 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"
// />
// )
// buttonText = (`${intl.formatMessage(messages.nextButton)} (${intl.formatMessage(messages.endOfCourse)})`);
// }
return (
<Button variant="outline-primary" className="next-button" onClick={buttonOnClick} disabled={disabled}>
@@ -95,11 +69,7 @@ export default function UnitNavigation(props) {
onClick={onClickPrevious}
>
<FontAwesomeIcon icon={faChevronLeft} className="mr-2" size="sm" />
<FormattedMessage
id="learn.sequence.navigation.after.unit.previous"
description="The button to go to the previous unit"
defaultMessage="Previous"
/>
{intl.formatMessage(messages.previousButton)}
</Button>
{renderNextButton()}
</div>
@@ -107,6 +77,7 @@ export default function UnitNavigation(props) {
}
UnitNavigation.propTypes = {
intl: intlShape.isRequired,
sequenceId: PropTypes.string.isRequired,
unitId: PropTypes.string,
onClickPrevious: PropTypes.func.isRequired,
@@ -117,3 +88,5 @@ UnitNavigation.propTypes = {
UnitNavigation.defaultProps = {
unitId: null,
};
export default injectIntl(UnitNavigation);

View File

@@ -22,6 +22,7 @@ describe('Unit Navigation', () => {
sequenceId: courseware.sequenceId,
onClickPrevious: () => {},
onClickNext: () => {},
goToCourseExitPage: () => {},
};
});

View File

@@ -0,0 +1,25 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
completeCourseButton: {
id: 'learn.sequence.navigation.complete.button',
defaultMessage: 'Complete the course',
description: 'Button to advance to the course completion page',
},
endOfCourse: {
id: 'learn.end.of.course',
defaultMessage: "You've reached the end of this course!",
},
nextButton: {
id: 'learn.sequence.navigation.next.button',
defaultMessage: 'Next',
description: 'Button to advance to the next section',
},
previousButton: {
id: 'learn.sequence.navigation.previous.button',
defaultMessage: 'Previous',
description: 'Button to return to the previous section',
},
});
export default messages;

View File

@@ -246,7 +246,9 @@ $primary: #1176B2;
}
}
@media (min-width: map-get($grid-breakpoints, 'sm')) {
min-width: 10rem;
min-width: fit-content;
padding-left: 2rem;
padding-right: 2rem;
}
}