Compare commits
3 Commits
dependabot
...
ddumesnil/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48ae83084b | ||
|
|
6d524f535d | ||
|
|
cb1ea7ad9f |
@@ -18,7 +18,7 @@ const messages = defineMessages({
|
||||
},
|
||||
success: {
|
||||
id: 'learning.enrollment.success',
|
||||
defaultMessage: "You've successfully enrolled in this course!",
|
||||
defaultMessage: 'You’ve successfully enrolled in this course!',
|
||||
description: 'A message telling the user that their course enrollment was successful.',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
const messages = defineMessages({
|
||||
'datesBanner.datesTabInfoBanner.header': {
|
||||
id: 'datesBanner.datesTabInfoBanner.header',
|
||||
defaultMessage: "We've built a suggested schedule to help you stay on track. ",
|
||||
defaultMessage: 'We’ve built a suggested schedule to help you stay on track. ',
|
||||
description: 'Strong text in Dates Tab Info Banner',
|
||||
},
|
||||
'datesBanner.datesTabInfoBanner.body': {
|
||||
@@ -35,8 +35,8 @@ const messages = defineMessages({
|
||||
},
|
||||
'datesBanner.upgradeToResetBanner.body': {
|
||||
id: 'datesBanner.upgradeToResetBanner.body',
|
||||
defaultMessage: `which means that you are unable to participate in graded assignments. It looks like you missed
|
||||
some important deadlines based on our suggested schedule. To complete graded assignments as part of this course
|
||||
defaultMessage: `which means that you are unable to participate in graded assignments. It looks like you missed
|
||||
some important deadlines based on our suggested schedule. To complete graded assignments as part of this course
|
||||
and shift the past due assignments into the future, you can upgrade today.`,
|
||||
description: 'Body in Upgrade To Reset Banner',
|
||||
},
|
||||
@@ -52,7 +52,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'datesBanner.resetDatesBanner.body': {
|
||||
id: 'datesBanner.resetDatesBanner.body',
|
||||
defaultMessage: `To keep yourself on track, you can update this schedule and shift the past due assignments into
|
||||
defaultMessage: `To keep yourself on track, you can update this schedule and shift the past due assignments into
|
||||
the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.`,
|
||||
description: 'Body in Reset Dates Banner',
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ import useOfferAlert from '../../alerts/offer-alert';
|
||||
|
||||
import Sequence from './sequence';
|
||||
|
||||
import { CelebrationModal, shouldCelebrateOnSectionLoad } from './celebration';
|
||||
import { FirstSectionCelebrationModal, shouldCelebrateOnSectionLoad } from './celebration';
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
import CourseSock from './course-sock';
|
||||
import ContentTools from './content-tools';
|
||||
@@ -78,7 +78,7 @@ function Course({
|
||||
previousSequenceHandler={previousSequenceHandler}
|
||||
/>
|
||||
{celebrationOpen && (
|
||||
<CelebrationModal
|
||||
<FirstSectionCelebrationModal
|
||||
courseId={courseId}
|
||||
open
|
||||
/>
|
||||
|
||||
@@ -71,9 +71,9 @@ describe('Course', () => {
|
||||
handleNextSectionCelebration(sequenceId, sequenceId, testData.unitId);
|
||||
render(<Course {...testData} />, { store: testStore });
|
||||
|
||||
const celebrationModal = screen.getByRole('dialog');
|
||||
expect(celebrationModal).toBeInTheDocument();
|
||||
expect(getByRole(celebrationModal, 'heading', { name: 'Congratulations!' })).toBeInTheDocument();
|
||||
const FirstSectionCelebrationModal = screen.getByRole('dialog');
|
||||
expect(FirstSectionCelebrationModal).toBeInTheDocument();
|
||||
expect(getByRole(FirstSectionCelebrationModal, 'heading', { name: 'Congratulations!' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays upgrade sock', async () => {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Modal } from '@edx/paragon';
|
||||
import { layoutGenerator } from 'react-break';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import DiscussionMobile from './assets/FirstDiscussion_mobile.png';
|
||||
import DiscussionTablet from './assets/FirstDiscussion_desktop_500.png';
|
||||
import messages from './messages';
|
||||
import { recordFirstDiscussionCelebration } from './utils';
|
||||
import { updateModel } from '../../../generic/model-store';
|
||||
|
||||
function FirstDiscussionCelebrationModal({
|
||||
courseId, firstDiscussionUserBucket, intl, open, ...rest
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const layout = layoutGenerator({
|
||||
mobile: 0,
|
||||
tablet: 400,
|
||||
});
|
||||
|
||||
const OnMobile = layout.is('mobile');
|
||||
const OnAtLeastTablet = layout.isAtLeast('tablet');
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
recordFirstDiscussionCelebration(courseId);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
let normativeDataBodyText;
|
||||
// Bucket 2 corresponds to showing normative data in the body text
|
||||
if (firstDiscussionUserBucket === 2) {
|
||||
if (Math.random() > 0.5) {
|
||||
normativeDataBodyText = (<p className="mt-3">{intl.formatMessage(messages.discussionBodyText1)}</p>);
|
||||
} else {
|
||||
normativeDataBodyText = (<p className="mt-3">{intl.formatMessage(messages.discussionBodyText2)}</p>);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('normativeDataBodyText:', normativeDataBodyText);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
body={(
|
||||
<>
|
||||
<p>{intl.formatMessage(messages.conversation)}</p>
|
||||
<OnMobile>
|
||||
<img src={DiscussionMobile} alt="" className="img-fluid" />
|
||||
</OnMobile>
|
||||
<OnAtLeastTablet>
|
||||
<img src={DiscussionTablet} alt="" className="img-fluid" />
|
||||
</OnAtLeastTablet>
|
||||
{normativeDataBodyText}
|
||||
</>
|
||||
)}
|
||||
closeText={intl.formatMessage(messages.keepItUp)}
|
||||
onClose={() => {
|
||||
// Update our local copy of course data from LMS
|
||||
console.log('updating model');
|
||||
dispatch(updateModel({
|
||||
modelType: 'courses',
|
||||
model: {
|
||||
id: courseId,
|
||||
celebrations: {
|
||||
firstDiscussion: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}}
|
||||
open={open}
|
||||
title={intl.formatMessage(messages.firstDiscussionPost)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
FirstDiscussionCelebrationModal.defaultProps = {
|
||||
open: false,
|
||||
firstDiscussionUserBucket: 2,
|
||||
};
|
||||
|
||||
FirstDiscussionCelebrationModal.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
firstDiscussionUserBucket: PropTypes.number,
|
||||
intl: intlShape.isRequired,
|
||||
open: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(FirstDiscussionCelebrationModal);
|
||||
@@ -10,7 +10,7 @@ import messages from './messages';
|
||||
import SocialIcons from '../../social-share/SocialIcons';
|
||||
import { recordFirstSectionCelebration } from './utils';
|
||||
|
||||
function CelebrationModal({
|
||||
function FirstSectionCelebrationModal({
|
||||
courseId, intl, open, ...rest
|
||||
}) {
|
||||
const layout = layoutGenerator({
|
||||
@@ -58,14 +58,14 @@ function CelebrationModal({
|
||||
);
|
||||
}
|
||||
|
||||
CelebrationModal.defaultProps = {
|
||||
FirstSectionCelebrationModal.defaultProps = {
|
||||
open: false,
|
||||
};
|
||||
|
||||
CelebrationModal.propTypes = {
|
||||
FirstSectionCelebrationModal.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
open: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(CelebrationModal);
|
||||
export default injectIntl(FirstSectionCelebrationModal);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 961 B |
Binary file not shown.
|
After Width: | Height: | Size: 742 B |
@@ -4,9 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
// Does not block on answer
|
||||
export function postFirstSectionCelebrationComplete(courseId) {
|
||||
export function postCelebrationComplete(courseId, payload) {
|
||||
const url = new URL(`${getConfig().LMS_BASE_URL}/api/courseware/celebration/${courseId}`);
|
||||
getAuthenticatedHttpClient().post(url.href, {
|
||||
first_section: false,
|
||||
});
|
||||
getAuthenticatedHttpClient().post(url.href, payload);
|
||||
}
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
export { default as CelebrationModal } from './CelebrationModal';
|
||||
export { handleNextSectionCelebration, shouldCelebrateOnSectionLoad } from './utils';
|
||||
export { default as FirstDiscussionCelebrationModal } from './FirstDiscussionCelebrationModal';
|
||||
export { default as FirstSectionCelebrationModal } from './FirstSectionCelebrationModal';
|
||||
export {
|
||||
handleNextSectionCelebration,
|
||||
shouldCelebrateOnSectionLoad,
|
||||
shouldCelebrateOnDiscussionPost,
|
||||
} from './utils';
|
||||
|
||||
@@ -9,20 +9,41 @@ const messages = defineMessages({
|
||||
id: 'learning.celebration.congrats',
|
||||
defaultMessage: 'Congratulations!',
|
||||
},
|
||||
conversation: {
|
||||
id: 'learning.celebration.conversation',
|
||||
defaultMessage: 'Nice job being part of the conversation.',
|
||||
},
|
||||
discussionBodyText1: {
|
||||
id: 'learning.celebration.discussionBodyText1',
|
||||
defaultMessage: 'Learners who participate in discussions are 10x more likely to complete their course than those who don’t.',
|
||||
},
|
||||
discussionBodyText2: {
|
||||
id: 'learning.celebration.discussionBodyText2',
|
||||
defaultMessage: 'Learners who participate in discussions complete 3x as much course content on average vs. those who don’t.',
|
||||
},
|
||||
earned: {
|
||||
id: 'learning.celebration.earned',
|
||||
defaultMessage: 'You earned it!',
|
||||
},
|
||||
emailSubject: {
|
||||
id: 'learning.celebration.emailSubject',
|
||||
defaultMessage: "I'm on my way to completing {title} online with {platform}!",
|
||||
defaultMessage: 'I’m on my way to completing {title} online with {platform}!',
|
||||
description: 'Subject when sharing course progress via email',
|
||||
},
|
||||
firstDiscussionPost: {
|
||||
id: 'learning.celebration.firstDiscussionPost',
|
||||
defaultMessage: 'First Discussion post!',
|
||||
},
|
||||
forward: {
|
||||
id: 'learning.celebration.forward',
|
||||
defaultMessage: 'Keep going',
|
||||
description: 'Button to close celebration dialog and get back to course',
|
||||
},
|
||||
keepItUp: {
|
||||
id: 'learning.celebration.keepItUp',
|
||||
defaultMessage: 'Keep it up',
|
||||
description: 'Button to close celebration dialog and get back to course',
|
||||
},
|
||||
share: {
|
||||
id: 'learning.celebration.share',
|
||||
defaultMessage: 'Take a moment to celebrate and share your progress.',
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { postFirstSectionCelebrationComplete } from './data/api';
|
||||
import { postCelebrationComplete } from './data/api';
|
||||
import { clearLocalStorage, getLocalStorage, setLocalStorage } from '../../../data/localStorage';
|
||||
import { updateModel } from '../../../generic/model-store';
|
||||
|
||||
const CELEBRATION_LOCAL_STORAGE_KEY = 'CelebrationModal.showOnSectionLoad';
|
||||
const CELEBRATION_LOCAL_STORAGE_KEY = 'FirstSectionCelebrationModal.showOnSectionLoad';
|
||||
|
||||
// Records clicks through the end of a section, so that we can know whether we should celebrate when we finish loading
|
||||
function handleNextSectionCelebration(sequenceId, nextSequenceId, nextUnitId) {
|
||||
@@ -16,18 +16,34 @@ function handleNextSectionCelebration(sequenceId, nextSequenceId, nextUnitId) {
|
||||
});
|
||||
}
|
||||
|
||||
function recordFirstSectionCelebration(courseId) {
|
||||
// Tell the LMS
|
||||
postFirstSectionCelebrationComplete(courseId);
|
||||
|
||||
// Tell our analytics
|
||||
function sendCelebrationSegmentEvent(courseId, eventName) {
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
sendTrackEvent('edx.ui.lms.celebration.first_section.opened', {
|
||||
sendTrackEvent(eventName, {
|
||||
course_id: courseId,
|
||||
is_staff: administrator,
|
||||
});
|
||||
}
|
||||
|
||||
function recordFirstSectionCelebration(courseId) {
|
||||
// Tell the LMS
|
||||
postCelebrationComplete(courseId, { first_section: false });
|
||||
|
||||
// Tell our analytics
|
||||
sendCelebrationSegmentEvent(courseId, 'edx.ui.lms.celebration.first_section.opened');
|
||||
}
|
||||
|
||||
function recordFirstDiscussionCelebration(courseId) {
|
||||
/* postCelebrationComplete should start being used once the Discussion MFE exists
|
||||
and we no longer record the first discussion post in the JS handler in edx-platform
|
||||
See edx-platform/common/static/common/js/discussion/views/new_post_view.js and
|
||||
edx-platform/cms/static/common/js/discussion/views/discussion_thread_view.js */
|
||||
// Tell the LMS
|
||||
// postCelebrationComplete(courseId, { first_discussion: false });
|
||||
|
||||
// Tell our analytics
|
||||
sendCelebrationSegmentEvent(courseId, 'edx.ui.lms.celebration.first_discussion.opened');
|
||||
}
|
||||
|
||||
// Looks at local storage to see whether we just came from the end of a section.
|
||||
// Note! This does have side effects (will clear some local storage and may start an api call).
|
||||
function shouldCelebrateOnSectionLoad(courseId, sequenceId, unitId, celebrateFirstSection, dispatch) {
|
||||
@@ -63,4 +79,16 @@ function shouldCelebrateOnSectionLoad(courseId, sequenceId, unitId, celebrateFir
|
||||
return shouldCelebrate;
|
||||
}
|
||||
|
||||
export { handleNextSectionCelebration, recordFirstSectionCelebration, shouldCelebrateOnSectionLoad };
|
||||
function shouldCelebrateOnDiscussionPost(firstDiscussion, firstDiscussionUserBucket) {
|
||||
// Bucket 0 === Control group which does not get the discussion celebration.
|
||||
// That check can be removed when we stop using the flag as an experiment.
|
||||
return firstDiscussion && firstDiscussionUserBucket !== 0;
|
||||
}
|
||||
|
||||
export {
|
||||
handleNextSectionCelebration,
|
||||
recordFirstDiscussionCelebration,
|
||||
recordFirstSectionCelebration,
|
||||
shouldCelebrateOnSectionLoad,
|
||||
shouldCelebrateOnDiscussionPost,
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ const messages = defineMessages({
|
||||
dashboardLink: {
|
||||
id: 'courseExit.dashboardLink',
|
||||
defaultMessage: 'Dashboard',
|
||||
description: "Link to user's dashboard",
|
||||
description: 'Link to user’s dashboard',
|
||||
},
|
||||
downloadButton: {
|
||||
id: 'courseCelebration.downloadButton',
|
||||
@@ -69,7 +69,7 @@ const messages = defineMessages({
|
||||
linkedinAddToProfileButton: {
|
||||
id: 'courseCelebration.linkedinAddToProfileButton',
|
||||
defaultMessage: 'Add to LinkedIn profile',
|
||||
description: "Button to add certificate information to the user's LinkedIn profile",
|
||||
description: 'Button to add certificate information to the user’s LinkedIn profile',
|
||||
},
|
||||
nextButtonComplete: {
|
||||
id: 'learn.sequence.navigation.complete.button', // for historical reasons
|
||||
@@ -82,7 +82,7 @@ const messages = defineMessages({
|
||||
profileLink: {
|
||||
id: 'courseExit.profileLink',
|
||||
defaultMessage: 'Profile',
|
||||
description: "Link to user's profile",
|
||||
description: 'Link to user’s profile',
|
||||
},
|
||||
requestCertificateBodyText: {
|
||||
id: 'courseCelebration.requestCertificateBodyText',
|
||||
|
||||
@@ -12,6 +12,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Modal } from '@edx/paragon';
|
||||
import messages from './messages';
|
||||
import BookmarkButton from '../bookmark/BookmarkButton';
|
||||
import { FirstDiscussionCelebrationModal, shouldCelebrateOnDiscussionPost } from '../celebration';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import PageLoading from '../../../generic/PageLoading';
|
||||
import { processEvent } from '../../../course-home/data/thunks';
|
||||
@@ -65,15 +66,21 @@ function Unit({
|
||||
|
||||
const [iframeHeight, setIframeHeight] = useState(0);
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
const [discussionPosted, setDiscussionPosted] = useState(false);
|
||||
const [modalOptions, setModalOptions] = useState({ open: false });
|
||||
|
||||
const unit = useModel('units', id);
|
||||
const course = useModel('courses', courseId);
|
||||
const {
|
||||
celebrations: {
|
||||
firstDiscussion,
|
||||
firstDiscussionUserBucket,
|
||||
},
|
||||
contentTypeGatingEnabled,
|
||||
} = course;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const shouldCelebrateDiscussionPost = shouldCelebrateOnDiscussionPost(firstDiscussion, firstDiscussionUserBucket);
|
||||
|
||||
// Do not remove this hook. See function description.
|
||||
useLoadBearingHook(id);
|
||||
@@ -109,6 +116,9 @@ function Unit({
|
||||
return () => global.removeEventListener('message', messageEventListenerRef.current);
|
||||
}, [id, setIframeHeight, hasLoaded, iframeHeight, setHasLoaded, onLoaded]);
|
||||
|
||||
console.log('shouldCelebrateDiscussionPost:', shouldCelebrateDiscussionPost);
|
||||
console.log('discussionPosted:', discussionPosted);
|
||||
|
||||
return (
|
||||
<div className="unit">
|
||||
<h2 className="mb-0 h4">{unit.title}</h2>
|
||||
@@ -157,6 +167,13 @@ function Unit({
|
||||
dialogClassName="modal-lg"
|
||||
/>
|
||||
)}
|
||||
{discussionPosted && shouldCelebrateDiscussionPost && (
|
||||
<FirstDiscussionCelebrationModal
|
||||
courseId={courseId}
|
||||
firstDiscussionUserBucket={firstDiscussionUserBucket}
|
||||
open
|
||||
/>
|
||||
)}
|
||||
<div className="unit-iframe-wrapper">
|
||||
<iframe
|
||||
id="unit-iframe"
|
||||
@@ -167,9 +184,11 @@ function Unit({
|
||||
scrolling="no"
|
||||
referrerPolicy="origin"
|
||||
onLoad={() => {
|
||||
window.onmessage = function handleResetDates(e) {
|
||||
if (e.data.event_name) {
|
||||
window.onmessage = function handleMessageEvent(e) {
|
||||
if (e.data.event_name === 'post_event') {
|
||||
dispatch(processEvent(e.data, fetchCourse));
|
||||
} else if (e.data.event_name === 'discussion_post') {
|
||||
setDiscussionPosted(true);
|
||||
}
|
||||
};
|
||||
}}
|
||||
|
||||
@@ -8,7 +8,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'learn.contentLock.complete.prerequisite': {
|
||||
id: 'learn.contentLock.complete.prerequisite',
|
||||
defaultMessage: "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
|
||||
defaultMessage: 'You must complete the prerequisite: "{prereqSectionName}" to access this content.',
|
||||
description: 'Message shown to indicate which prerequisite the student must complete prior to accessing the locked content. {prereqSectionName} is the name of the prerequisite.',
|
||||
},
|
||||
'learn.contentLock.goToSection': {
|
||||
|
||||
Reference in New Issue
Block a user