diff --git a/src/data/api.js b/src/data/api.js
index 1fda3d98..38ad54f6 100644
--- a/src/data/api.js
+++ b/src/data/api.js
@@ -40,12 +40,30 @@ function normalizeMetadata(metadata) {
};
}
+function normalizeCourseHomeCourseMetadata(metadata) {
+ const data = camelCaseObject(metadata);
+ return {
+ ...data,
+ tabs: data.tabs.map(tab => ({
+ slug: tab.tabId,
+ title: tab.title,
+ url: tab.url,
+ })),
+ };
+}
+
export async function getCourseMetadata(courseId) {
const url = `${getConfig().LMS_BASE_URL}/api/courseware/course/${courseId}`;
const { data } = await getAuthenticatedHttpClient().get(url);
return normalizeMetadata(data);
}
+export async function getCourseHomeCourseMetadata(courseId) {
+ const url = `${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/${courseId}`;
+ const { data } = await getAuthenticatedHttpClient().get(url);
+ return normalizeCourseHomeCourseMetadata(data);
+}
+
export async function getDatesTabData(courseId, version) {
const url = `${getConfig().LMS_BASE_URL}/api/course_home/${version}/dates/${courseId}`;
try {
@@ -208,3 +226,8 @@ export async function getResumeBlock(courseId) {
const { data } = await getAuthenticatedHttpClient().get(url.href, {});
return camelCaseObject(data);
}
+
+export async function updateCourseDeadlines(courseId) {
+ const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`);
+ await getAuthenticatedHttpClient().post(url.href, { course_key: courseId });
+}
diff --git a/src/data/thunks.js b/src/data/thunks.js
index 51396474..fe551b3f 100644
--- a/src/data/thunks.js
+++ b/src/data/thunks.js
@@ -5,6 +5,8 @@ import {
getSequenceMetadata,
getDatesTabData,
getOutlineTabData,
+ getCourseHomeCourseMetadata,
+ updateCourseDeadlines,
} from './api';
import {
addModelsMap, updateModel, updateModels, updateModelsMap, addModel,
@@ -96,9 +98,11 @@ export function fetchTab(courseId, tab, version, getTabData) {
dispatch(fetchTabRequest({ courseId }));
Promise.allSettled([
getCourseMetadata(courseId),
+ getCourseHomeCourseMetadata(courseId),
getTabData(courseId, version),
- ]).then(([courseMetadataResult, tabDataResult]) => {
+ ]).then(([courseMetadataResult, courseHomeCourseMetadataResult, tabDataResult]) => {
const fetchedMetadata = courseMetadataResult.status === 'fulfilled';
+ const fetchedCourseHomeCourseMetadata = courseHomeCourseMetadataResult.status === 'fulfilled';
const fetchedTabData = tabDataResult.status === 'fulfilled';
if (fetchedMetadata) {
@@ -111,8 +115,18 @@ export function fetchTab(courseId, tab, version, getTabData) {
modelType: 'courses',
model: courseMetadataResult.value,
}));
+ }
+
+ if (fetchedCourseHomeCourseMetadata) {
+ dispatch(addModel({
+ modelType: 'courseHomeMetadata',
+ model: {
+ id: courseId,
+ ...courseHomeCourseMetadataResult.value,
+ },
+ }));
} else {
- logError(courseMetadataResult.reason);
+ logError(courseHomeCourseMetadataResult.reason);
}
if (fetchedTabData) {
@@ -127,7 +141,7 @@ export function fetchTab(courseId, tab, version, getTabData) {
logError(tabDataResult.reason);
}
- if (fetchedMetadata && fetchedTabData) {
+ if (fetchedMetadata && fetchedCourseHomeCourseMetadata && fetchedTabData) {
dispatch(fetchTabSuccess({ courseId }));
} else {
dispatch(fetchTabFailure({ courseId }));
@@ -164,3 +178,11 @@ export function fetchSequence(sequenceId) {
}
};
}
+
+export function resetDeadlines(courseId, getTabData) {
+ return async (dispatch) => {
+ updateCourseDeadlines(courseId).then(() => {
+ dispatch(getTabData(courseId));
+ });
+ };
+}
diff --git a/src/dates-banner/DatesBanner.jsx b/src/dates-banner/DatesBanner.jsx
new file mode 100644
index 00000000..6c8d4a0d
--- /dev/null
+++ b/src/dates-banner/DatesBanner.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
+import messages from './messages';
+
+function DatesBanner(props) {
+ const {
+ intl,
+ name,
+ bannerClickHandler,
+ } = props;
+
+ return (
+
+
+
+
+ {intl.formatMessage(messages[`datesBanner.${name}.header`])}
+
+ {intl.formatMessage(messages[`datesBanner.${name}.body`])}
+
+ {bannerClickHandler && (
+
+ )}
+
+
+ );
+}
+
+DatesBanner.propTypes = {
+ intl: intlShape.isRequired,
+ name: PropTypes.string.isRequired,
+ bannerClickHandler: PropTypes.func,
+};
+
+DatesBanner.defaultProps = {
+ bannerClickHandler: null,
+};
+
+export default injectIntl(DatesBanner);
diff --git a/src/dates-banner/DatesBannerContainer.jsx b/src/dates-banner/DatesBannerContainer.jsx
new file mode 100644
index 00000000..a95fbb2a
--- /dev/null
+++ b/src/dates-banner/DatesBannerContainer.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { useModel } from '../model-store';
+
+import DatesBanner from './DatesBanner';
+import { fetchDatesTab, resetDeadlines } from '../data/thunks';
+
+function DatesBannerContainer(props) {
+ const {
+ model,
+ } = props;
+
+ const {
+ courseId,
+ } = useSelector(state => state.courseware);
+
+ const {
+ datesBannerInfo,
+ } = useModel(model, courseId);
+
+ const {
+ contentTypeGatingEnabled,
+ missedDeadlines,
+ missedGatedContent,
+ verifiedUpgradeLink,
+ } = datesBannerInfo;
+
+ const {
+ isSelfPaced,
+ } = useModel('courseHomeMetadata', courseId);
+
+ const dispatch = useDispatch();
+ const upgradeToCompleteGraded = model === 'dates' && contentTypeGatingEnabled && !missedDeadlines;
+ const upgradeToReset = !upgradeToCompleteGraded && missedDeadlines && missedGatedContent;
+ const resetDates = !upgradeToCompleteGraded && missedDeadlines && !missedGatedContent;
+ const datesBanners = [
+ {
+ name: 'datesTabInfoBanner',
+ shouldDisplay: model === 'dates' && !missedDeadlines && isSelfPaced,
+ },
+ {
+ name: 'upgradeToCompleteGradedBanner',
+ shouldDisplay: upgradeToCompleteGraded,
+ clickHandler: () => window.location.replace(verifiedUpgradeLink),
+ },
+ {
+ name: 'upgradeToResetBanner',
+ shouldDisplay: upgradeToReset,
+ clickHandler: () => window.location.replace(verifiedUpgradeLink),
+ },
+ {
+ name: 'resetDatesBanner',
+ shouldDisplay: resetDates,
+ clickHandler: () => dispatch(resetDeadlines(courseId, fetchDatesTab)),
+ },
+ ];
+
+ return (
+ <>
+ {datesBanners.map((banner) => banner.shouldDisplay && (
+
+ ))}
+ >
+ );
+}
+
+DatesBannerContainer.propTypes = {
+ model: PropTypes.string.isRequired,
+};
+
+export default DatesBannerContainer;
diff --git a/src/dates-banner/messages.js b/src/dates-banner/messages.js
new file mode 100644
index 00000000..cae3f829
--- /dev/null
+++ b/src/dates-banner/messages.js
@@ -0,0 +1,66 @@
+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. ",
+ description: 'Strong text in Dates Tab Info Banner',
+ },
+ 'datesBanner.datesTabInfoBanner.body': {
+ id: 'datesBanner.datesTabInfoBanner.body',
+ defaultMessage: `But don't worry—it's flexible so you can learn at your own pace. If you happen to fall behind on
+ our suggested dates, you'll be able to adjust them to keep yourself on track.`,
+ description: 'Body in Dates Tab Info Banner',
+ },
+ 'datesBanner.upgradeToCompleteGradedBanner.header': {
+ id: 'datesBanner.upgradeToCompleteGradedBanner.header',
+ defaultMessage: 'You are auditing this course, ',
+ description: 'Strong text in Upgrade To Complete Graded Banner',
+ },
+ 'datesBanner.upgradeToCompleteGradedBanner.body': {
+ id: 'datesBanner.upgradeToCompleteGradedBanner.body',
+ defaultMessage: `which means that you are unable to participate in graded assignments. To complete graded
+ assignments as part of this course, you can upgrade today.`,
+ description: 'Body in Upgrade To Complete Graded Banner',
+ },
+ 'datesBanner.upgradeToCompleteGradedBanner.button': {
+ id: 'datesBanner.upgradeToCompleteGradedBanner.button',
+ defaultMessage: 'Upgrade now',
+ description: 'Button in Upgrade To Complete Graded Banner',
+ },
+ 'datesBanner.upgradeToResetBanner.header': {
+ id: 'datesBanner.upgradeToResetBanner.header',
+ defaultMessage: 'You are auditing this course, ',
+ description: 'Strong text in Upgrade To Reset Banner',
+ },
+ '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
+ and shift the past due assignments into the future, you can upgrade today.`,
+ description: 'Body in Upgrade To Reset Banner',
+ },
+ 'datesBanner.upgradeToResetBanner.button': {
+ id: 'datesBanner.upgradeToResetBanner.button',
+ defaultMessage: 'Upgrade to shift due dates',
+ description: 'Button in Upgrade To Reset Banner',
+ },
+ 'datesBanner.resetDatesBanner.header': {
+ id: 'datesBanner.resetDatesBanner.header',
+ defaultMessage: 'It looks like you missed some important deadlines based on our suggested schedule. ',
+ description: 'Strong text in Reset Dates Banner',
+ },
+ '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
+ 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',
+ },
+ 'datesBanner.resetDatesBanner.button': {
+ id: 'datesBanner.resetDatesBanner.button',
+ defaultMessage: 'Reset my deadlines',
+ description: 'Button in Reset Dates Banner',
+ },
+});
+
+export default messages;
diff --git a/src/dates-tab/DatesTab.jsx b/src/dates-tab/DatesTab.jsx
index 088d2685..54403e9f 100644
--- a/src/dates-tab/DatesTab.jsx
+++ b/src/dates-tab/DatesTab.jsx
@@ -3,10 +3,12 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
import Timeline from './Timeline';
+import DatesBannerContainer from '../dates-banner/DatesBannerContainer';
function DatesTab({ intl }) {
return (
<>
+
{intl.formatMessage(messages.title)}