diff --git a/src/alerts/course-end-alert/CourseEndAlert.jsx b/src/alerts/course-end-alert/CourseEndAlert.jsx
new file mode 100644
index 00000000..1328ff73
--- /dev/null
+++ b/src/alerts/course-end-alert/CourseEndAlert.jsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ FormattedDate,
+ FormattedMessage,
+ FormattedRelative,
+ FormattedTime,
+} from '@edx/frontend-platform/i18n';
+
+import { Alert, ALERT_TYPES } from '../../generic/user-messages';
+
+const DAY_MS = 24 * 60 * 60 * 1000; // in ms
+
+function CourseEndAlert({ payload }) {
+ const {
+ delta,
+ description,
+ endDate,
+ userTimezone,
+ } = payload;
+
+ const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
+
+ const timeRemaining = (
+
+ );
+
+ let msg;
+ if (delta < DAY_MS) {
+ const courseEndTime = (
+
+ );
+ msg = (
+
+ );
+ } else {
+ const courseEndDate = (
+
+ );
+ msg = (
+
+ );
+ }
+
+ return (
+
+ {msg}
+ {description}
+
+ );
+}
+
+CourseEndAlert.propTypes = {
+ payload: PropTypes.shape({
+ delta: PropTypes.number,
+ description: PropTypes.string,
+ endDate: PropTypes.string,
+ userTimezone: PropTypes.string,
+ }).isRequired,
+};
+
+export default CourseEndAlert;
diff --git a/src/alerts/course-end-alert/hooks.js b/src/alerts/course-end-alert/hooks.js
new file mode 100644
index 00000000..101efeed
--- /dev/null
+++ b/src/alerts/course-end-alert/hooks.js
@@ -0,0 +1,41 @@
+/* eslint-disable import/prefer-default-export */
+import React from 'react';
+import { useAlert } from '../../generic/user-messages';
+import { useModel } from '../../generic/model-store';
+
+const CourseEndAlert = React.lazy(() => import('./CourseEndAlert'));
+
+// period of time (in ms) before end of course during which we alert
+const WARNING_PERIOD_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
+
+export function useCourseEndAlert(courseId) {
+ const {
+ isEnrolled,
+ } = useModel('courses', courseId);
+ const {
+ datesWidget: {
+ courseDateBlocks,
+ userTimezone,
+ },
+ } = useModel('outline', courseId);
+
+ const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date');
+ const endDate = endBlock ? new Date(endBlock.date) : null;
+ const delta = endBlock ? endDate - new Date() : 0;
+ const isVisible = isEnrolled && endBlock && delta > 0 && delta < WARNING_PERIOD_MS;
+
+ useAlert(isVisible, {
+ code: 'clientCourseEndAlert',
+ payload: {
+ delta,
+ description: endBlock && endBlock.description,
+ endDate: endBlock && endBlock.date,
+ userTimezone,
+ },
+ topic: 'outline-course-alerts',
+ });
+
+ return {
+ clientCourseEndAlert: CourseEndAlert,
+ };
+}
diff --git a/src/alerts/course-end-alert/index.js b/src/alerts/course-end-alert/index.js
new file mode 100644
index 00000000..eb25c278
--- /dev/null
+++ b/src/alerts/course-end-alert/index.js
@@ -0,0 +1 @@
+export { useCourseEndAlert as default } from './hooks';
diff --git a/src/alerts/enrollment-alert/hooks.js b/src/alerts/enrollment-alert/hooks.js
index ccc23066..0f1e44a9 100644
--- a/src/alerts/enrollment-alert/hooks.js
+++ b/src/alerts/enrollment-alert/hooks.js
@@ -26,7 +26,7 @@ export function useEnrollmentAlert(courseId) {
topic: 'outline',
});
- return EnrollmentAlert;
+ return { clientEnrollmentAlert: EnrollmentAlert };
}
export function useEnrollClickHandler(courseId, successText) {
diff --git a/src/alerts/logistration-alert/hooks.js b/src/alerts/logistration-alert/hooks.js
index a2370a48..be804b1b 100644
--- a/src/alerts/logistration-alert/hooks.js
+++ b/src/alerts/logistration-alert/hooks.js
@@ -1,8 +1,10 @@
/* eslint-disable import/prefer-default-export */
-import { useContext } from 'react';
+import React, { useContext } from 'react';
import { AppContext } from '@edx/frontend-platform/react';
import { ALERT_TYPES, useAlert } from '../../generic/user-messages';
+const LogistrationAlert = React.lazy(() => import('./LogistrationAlert'));
+
export function useLogistrationAlert() {
const { authenticatedUser } = useContext(AppContext);
const isVisible = authenticatedUser === null;
@@ -13,4 +15,6 @@ export function useLogistrationAlert() {
dismissible: false,
type: ALERT_TYPES.ERROR,
});
+
+ return { clientLogistrationAlert: LogistrationAlert };
}
diff --git a/src/alerts/logistration-alert/index.js b/src/alerts/logistration-alert/index.js
index 7da16700..bd6ec9f8 100644
--- a/src/alerts/logistration-alert/index.js
+++ b/src/alerts/logistration-alert/index.js
@@ -1,2 +1 @@
-export { default } from './LogistrationAlert';
-export { useLogistrationAlert } from './hooks';
+export { useLogistrationAlert as default } from './hooks';
diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx
index d171e956..af250a52 100644
--- a/src/course-home/outline-tab/OutlineTab.jsx
+++ b/src/course-home/outline-tab/OutlineTab.jsx
@@ -10,13 +10,12 @@ import CourseHandouts from './widgets/CourseHandouts';
import CourseTools from './widgets/CourseTools';
import messages from './messages';
import Section from './Section';
+import useCourseEndAlert from '../../alerts/course-end-alert';
import useEnrollmentAlert from '../../alerts/enrollment-alert';
-import { useLogistrationAlert } from '../../alerts/logistration-alert';
+import useLogistrationAlert from '../../alerts/logistration-alert';
import { useModel } from '../../generic/model-store';
import WelcomeMessage from './widgets/WelcomeMessage';
-const LogistrationAlert = React.lazy(() => import('../../alerts/logistration-alert'));
-
function OutlineTab({ intl }) {
const {
courseId,
@@ -39,8 +38,9 @@ function OutlineTab({ intl }) {
},
} = useModel('outline', courseId);
- const clientEnrollmentAlert = useEnrollmentAlert(courseId);
- useLogistrationAlert();
+ const courseEndAlert = useCourseEndAlert(courseId);
+ const enrollmentAlert = useEnrollmentAlert(courseId);
+ const logistrationAlert = useLogistrationAlert();
const rootCourseId = Object.keys(courses)[0];
const { sectionIds } = courses[rootCourseId];
@@ -51,8 +51,8 @@ function OutlineTab({ intl }) {
topic="outline"
className="mb-3"
customAlerts={{
- clientEnrollmentAlert,
- clientLogistrationAlert: LogistrationAlert,
+ ...enrollmentAlert,
+ ...logistrationAlert,
}}
/>
@@ -62,6 +62,13 @@ function OutlineTab({ intl }) {
+
{sectionIds.map((sectionId) => (