- {intl.formatMessage(messages.completed)}
-
-
-
-
-
-
-
- {intl.formatMessage(messages.earned)} {intl.formatMessage(messages.share)}
-
-
- >
+
+ {intl.formatMessage(messages.forward)}
+
+ )}
+ hasCloseButton={false}
+ isOpen={isOpen}
+ onClose={onClose}
+ title={(
+ {intl.formatMessage(messages.congrats)}
)}
- closeText={intl.formatMessage(messages.forward)}
- onClose={() => {}} // Don't do anything special, just having the modal close is enough (this is a required prop)
- open={open}
- title={intl.formatMessage(messages.congrats)}
{...rest}
- />
+ >
+ <>
+ {intl.formatMessage(messages.completed)}
+
+
+
+
+
+
+
+ {intl.formatMessage(messages.earned)} {intl.formatMessage(messages.share)}
+
+
+ >
+
);
}
-CelebrationModal.defaultProps = {
- open: false,
-};
-
CelebrationModal.propTypes = {
courseId: PropTypes.string.isRequired,
intl: intlShape.isRequired,
- open: PropTypes.bool,
+ isOpen: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
};
export default injectIntl(CelebrationModal);
diff --git a/src/courseware/course/celebration/WeeklyGoalCelebrationModal.jsx b/src/courseware/course/celebration/WeeklyGoalCelebrationModal.jsx
new file mode 100644
index 00000000..402e72c7
--- /dev/null
+++ b/src/courseware/course/celebration/WeeklyGoalCelebrationModal.jsx
@@ -0,0 +1,80 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import {
+ ActionRow, Button, Icon, StandardModal,
+} from '@edx/paragon';
+import { Lightbulb } from '@edx/paragon/icons';
+
+import Target from './assets/target.svg';
+import messages from './messages';
+import { recordWeeklyGoalCelebration } from './utils';
+import { useModel } from '../../../generic/model-store';
+
+function WeeklyGoalCelebrationModal({
+ courseId, daysPerWeek, intl, isOpen, onClose, ...rest
+}) {
+ const { org } = useModel('coursewareMeta', courseId);
+
+ useEffect(() => {
+ if (isOpen) {
+ recordWeeklyGoalCelebration(org, courseId);
+ }
+ }, [isOpen]);
+
+ return (
+
+ {intl.formatMessage(messages.keepItUp)}
+
+ )}
+ hasCloseButton={false}
+ isOpen={isOpen}
+ onClose={onClose}
+ title={(
+ {intl.formatMessage(messages.goalMet)}
+ )}
+ {...rest}
+ >
+ <>
+
+ {daysPerWeek} {daysPerWeek === 1 ? 'time' : 'times'}),
+ }}
+ />
+
+
+
+
+
+
+ achieve higher performance),
+ }}
+ />
+
+ >
+
+ );
+}
+
+WeeklyGoalCelebrationModal.propTypes = {
+ courseId: PropTypes.string.isRequired,
+ daysPerWeek: PropTypes.number.isRequired,
+ intl: intlShape.isRequired,
+ isOpen: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
+
+export default injectIntl(WeeklyGoalCelebrationModal);
diff --git a/src/courseware/course/celebration/assets/target.svg b/src/courseware/course/celebration/assets/target.svg
new file mode 100644
index 00000000..37c2b3ae
--- /dev/null
+++ b/src/courseware/course/celebration/assets/target.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/courseware/course/celebration/data/api.js b/src/courseware/course/celebration/data/api.js
index 609ab5e7..4f50dc92 100644
--- a/src/courseware/course/celebration/data/api.js
+++ b/src/courseware/course/celebration/data/api.js
@@ -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, data) {
const url = new URL(`${getConfig().LMS_BASE_URL}/api/courseware/celebration/${courseId}`);
- getAuthenticatedHttpClient().post(url.href, {
- first_section: false,
- });
+ getAuthenticatedHttpClient().post(url.href, data);
}
diff --git a/src/courseware/course/celebration/index.js b/src/courseware/course/celebration/index.js
index 015f7d43..755bb23a 100644
--- a/src/courseware/course/celebration/index.js
+++ b/src/courseware/course/celebration/index.js
@@ -1,2 +1,3 @@
export { default as CelebrationModal } from './CelebrationModal';
+export { default as WeeklyGoalCelebrationModal } from './WeeklyGoalCelebrationModal';
export { handleNextSectionCelebration, shouldCelebrateOnSectionLoad } from './utils';
diff --git a/src/courseware/course/celebration/messages.js b/src/courseware/course/celebration/messages.js
index 8fa81c65..4f3f79d6 100644
--- a/src/courseware/course/celebration/messages.js
+++ b/src/courseware/course/celebration/messages.js
@@ -23,6 +23,15 @@ const messages = defineMessages({
defaultMessage: 'Keep going',
description: 'Button to close celebration dialog and get back to course',
},
+ goalMet: {
+ id: 'learning.celebration.goalMet',
+ defaultMessage: 'You met your goal!',
+ },
+ 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.',
diff --git a/src/courseware/course/celebration/utils.jsx b/src/courseware/course/celebration/utils.jsx
index 23662714..1c1c687a 100644
--- a/src/courseware/course/celebration/utils.jsx
+++ b/src/courseware/course/celebration/utils.jsx
@@ -1,7 +1,7 @@
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';
@@ -18,7 +18,7 @@ function handleNextSectionCelebration(sequenceId, nextSequenceId, nextUnitId) {
function recordFirstSectionCelebration(org, courseId) {
// Tell the LMS
- postFirstSectionCelebrationComplete(courseId);
+ postCelebrationComplete(courseId, { first_section: false });
// Tell our analytics
const { administrator } = getAuthenticatedUser();
@@ -30,6 +30,19 @@ function recordFirstSectionCelebration(org, courseId) {
});
}
+function recordWeeklyGoalCelebration(org, courseId) {
+ // Tell the LMS
+ postCelebrationComplete(courseId, { weekly_goal: false });
+
+ // Tell our analytics
+ const { administrator } = getAuthenticatedUser();
+ sendTrackEvent('edx.ui.lms.celebration.weekly_goal.opened', {
+ org_key: org,
+ courserun_key: courseId,
+ is_staff: administrator,
+ });
+}
+
// 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, celebrations) {
@@ -51,7 +64,7 @@ function shouldCelebrateOnSectionLoad(courseId, sequenceId, unitId, celebrateFir
// If we are going to celebrate a streak then we will not also celebrate the first section.
// We will still mark the first section as celebrated, so that we don't incorrectly celebrate the second section.
shouldCelebrate = false;
- postFirstSectionCelebrationComplete(courseId);
+ postCelebrationComplete(courseId, { first_section: false });
}
if (sequenceId !== prevSequenceId && !onTargetUnit) {
@@ -74,4 +87,9 @@ function shouldCelebrateOnSectionLoad(courseId, sequenceId, unitId, celebrateFir
return shouldCelebrate;
}
-export { handleNextSectionCelebration, recordFirstSectionCelebration, shouldCelebrateOnSectionLoad };
+export {
+ handleNextSectionCelebration,
+ recordFirstSectionCelebration,
+ recordWeeklyGoalCelebration,
+ shouldCelebrateOnSectionLoad,
+};
diff --git a/src/courseware/data/api.js b/src/courseware/data/api.js
index c927e4a6..1214ec20 100644
--- a/src/courseware/data/api.js
+++ b/src/courseware/data/api.js
@@ -198,7 +198,7 @@ function normalizeMetadata(metadata) {
accessExpiration: camelCaseObject(data.access_expiration),
canShowUpgradeSock: data.can_show_upgrade_sock,
contentTypeGatingEnabled: data.content_type_gating_enabled,
- courseGoals: data.course_goals,
+ courseGoals: camelCaseObject(data.course_goals),
id: data.id,
title: data.name,
number: data.number,