Lesson 1 - Getting Started > Getting Started",
+ "bookmarked": false,
+ "graded": false,
+ "page_title": "Getting Started",
+ "href": "",
+ "complete": true,
+ "content": "",
+ "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec"
+ },
+ "block-v1:edX+DemoX+Demo_Course+type@vertical+block@3e3f9b5199ba4e96b2fc6539087cfe2c": {
+ "type": "other",
+ "path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > MY unit",
+ "bookmarked": false,
+ "graded": false,
+ "page_title": "MY unit",
+ "href": "",
+ "complete": false,
+ "content": "",
+ "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@3e3f9b5199ba4e96b2fc6539087cfe2c"
+ },
+ "block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9": {
+ "type": "video",
+ "path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Working with Videos",
+ "bookmarked": false,
+ "graded": false,
+ "page_title": "Working with Videos",
+ "href": "",
+ "complete": false,
+ "content": "",
+ "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9"
+ },
+ "block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0": {
+ "type": "video",
+ "path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Videos on edX",
+ "bookmarked": false,
+ "graded": false,
+ "page_title": "Videos on edX",
+ "href": "",
+ "complete": false,
+ "content": "",
+ "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0"
+ },
+ "block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76": {
+ "type": "other",
+ "path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Video Demonstrations",
+ "bookmarked": false,
+ "graded": false,
+ "page_title": "Video Demonstrations",
+ "href": "",
+ "complete": false,
+ "content": "",
+ "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76"
+ },
+ "block-v1:edX+DemoX+Demo_Course+type@vertical+block@f0e6d90842c44cc7a50fd1a18a7dd982": {
+ "type": "video",
+ "path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Video Demonstrations",
+ "bookmarked": false,
+ "graded": false,
+ "page_title": "Video Demonstrations",
+ "href": "",
+ "complete": false,
+ "content": "",
+ "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@f0e6d90842c44cc7a50fd1a18a7dd982"
+ },
+ "block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0": {
+ "type": "video",
+ "path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Video Presentation Styles",
+ "bookmarked": false,
+ "graded": false,
+ "page_title": "Video Presentation Styles",
+ "href": "",
+ "complete": false,
+ "content": "",
+ "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0"
+ },
+ "block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1": {
+ "type": "problem",
+ "path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Interactive Questions",
+ "bookmarked": false,
+ "graded": false,
+ "page_title": "Interactive Questions",
+ "href": "",
+ "complete": false,
+ "content": "",
+ "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1"
+ },
+ "block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606": {
+ "type": "other",
+ "path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Exciting Labs and Tools",
+ "bookmarked": false,
+ "graded": false,
+ "page_title": "Exciting Labs and Tools",
+ "href": "",
+ "complete": false,
+ "content": "",
+ "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606"
+ },
+ "block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e": {
+ "type": "problem",
+ "path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Reading Assignments",
+ "bookmarked": false,
+ "graded": false,
+ "page_title": "Reading Assignments",
+ "href": "",
+ "complete": false,
+ "content": "",
+ "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e"
+ },
+ "block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193": {
+ "type": "other",
+ "path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > When Are Your Exams? ",
+ "bookmarked": false,
+ "graded": false,
+ "page_title": "When Are Your Exams? ",
+ "href": "",
+ "complete": false,
+ "content": "",
+ "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193"
+ },
+ }}
+ displayName="Sequence Name"
+ activeUnitId="block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec"
+ showCompletion={true}
+ isTimeLimited={false}
+ bannerText={null}
+ onNext={() => {}}
+ onPrevious={() => {}}
+ onNavigateUnit={() => {}}
+ isGated={false}
+ prerequisite={{
+ name: 'Prerequisite name',
+ url: 'url? or id',
+ id: 'asdasd',
+ }}
+ savePosition={true}
+ />
+ )}
diff --git a/src/learning-sequence/sequence/CompleteIcon.jsx b/src/learning-sequence/sequence/CompleteIcon.jsx
new file mode 100644
index 00000000..6bc7a0ef
--- /dev/null
+++ b/src/learning-sequence/sequence/CompleteIcon.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
+
+export default function CompleteIcon(props) {
+ return
+}
diff --git a/src/learning-sequence/sequence/Sequence.jsx b/src/learning-sequence/sequence/Sequence.jsx
new file mode 100644
index 00000000..7e9075e4
--- /dev/null
+++ b/src/learning-sequence/sequence/Sequence.jsx
@@ -0,0 +1,139 @@
+import React, { useState, useEffect, Suspense } from 'react';
+import PropTypes from 'prop-types';
+import Unit from './Unit';
+import SequenceNavigation from './SequenceNavigation';
+import PageLoading from '../PageLoading';
+import { getBlockCompletion, saveSequencePosition } from './api';
+const ContentLock = React.lazy(() => import('./content-lock'));
+import messages from './messages';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
+function Sequence({
+ courseId,
+ id,
+ unitIds,
+ units: initialUnits,
+ displayName,
+ showCompletion,
+ isTimeLimited,
+ bannerText,
+ onNext,
+ onPrevious,
+ onNavigateUnit,
+ isGated,
+ prerequisite,
+ savePosition,
+ activeUnitId: initialActiveUnitId,
+ intl,
+}) {
+ const [units, setUnits] = useState(initialUnits);
+ const [activeUnitId, setActiveUnitId] = useState(initialActiveUnitId);
+
+ const activeUnitIndex = unitIds.indexOf(activeUnitId);
+ const activeUnit = units[activeUnitId];
+ const unitsArr = unitIds.map((unitId) => ({
+ ...units[unitId],
+ id: unitId,
+ isActive: unitId === activeUnitId,
+ }));
+
+ const updateUnitCompletion = (unitId) => {
+ // If the unit is already complete, don't check.
+ if (units[unitId].complete) {
+ return;
+ }
+
+ getBlockCompletion(courseId, id, unitId).then((isComplete) => {
+ if (isComplete) {
+ setUnits({
+ ...units,
+ [unitId]: { ...units[unitId], complete: isComplete },
+ });
+ }
+ })
+ };
+
+ const handleNext = () => {
+ if (activeUnitIndex < unitIds.length - 1) {
+ handleNavigate(activeUnitIndex + 1);
+ } else {
+ onNext();
+ }
+ };
+
+ const handlePrevious = () => {
+ if (activeUnitIndex > 0) {
+ handleNavigate(activeUnitIndex - 1);
+ } else {
+ onPrevious();
+ }
+ };
+
+ const handleNavigate = (unitIndex) => {
+ const newUnitId = unitIds[unitIndex];
+ if (showCompletion) {
+ updateUnitCompletion(activeUnitId);
+ }
+ setActiveUnitId(newUnitId);
+ onNavigateUnit(newUnitId, units[newUnitId]);
+ };
+
+ useEffect(() => {
+ if (savePosition) {
+ saveSequencePosition(courseId, id, activeUnitIndex);
+ }
+ }, [activeUnitId]);
+
+ return (
+
+
+ {isGated? (
+ }>
+
+
+ ): (
+
+ )}
+
+ );
+}
+
+Sequence.propTypes = {
+ id: PropTypes.string.isRequired,
+ courseId: PropTypes.string.isRequired,
+ unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
+ units: PropTypes.objectOf(PropTypes.shape({
+ })),
+ displayName: PropTypes.string.isRequired,
+ activeUnitId: PropTypes.string.isRequired,
+ showCompletion: PropTypes.bool.isRequired,
+ isTimeLimited: PropTypes.bool.isRequired,
+ bannerText: PropTypes.string,
+ onNext: PropTypes.func.isRequired,
+ onPrevious: PropTypes.func.isRequired,
+ onNavigateUnit: PropTypes.func.isRequired,
+ isGated: PropTypes.bool.isRequired,
+ prerequisite: PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ url: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
+ }),
+ savePosition: PropTypes.bool.isRequired,
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(Sequence);
diff --git a/src/learning-sequence/sequence/SequenceNavigation.jsx b/src/learning-sequence/sequence/SequenceNavigation.jsx
new file mode 100644
index 00000000..fab7c5f5
--- /dev/null
+++ b/src/learning-sequence/sequence/SequenceNavigation.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { Button } from '@edx/paragon';
+import UnitButton from './UnitButton';
+
+export default function SequenceNavigation({
+ onNext,
+ onPrevious,
+ onNavigate,
+ units,
+ isLocked,
+ showCompletion,
+}) {
+ const unitButtons = units.map((unit, index) => (
+
+ ));
+
+ return (
+
+ );
+}
diff --git a/src/learning-sequence/sequence/Unit.jsx b/src/learning-sequence/sequence/Unit.jsx
new file mode 100644
index 00000000..207e87ae
--- /dev/null
+++ b/src/learning-sequence/sequence/Unit.jsx
@@ -0,0 +1,22 @@
+import React, { useRef } from 'react';
+import PropTypes from 'prop-types';
+import { getConfig } from '@edx/frontend-platform';
+
+export default function Unit({ id, title }) {
+ const iframeRef = useRef(null);
+ const iframeUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}`;
+
+ return (
+
+ );
+}
+
+Unit.propTypes = {
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+};
diff --git a/src/learning-sequence/sequence/UnitButton.jsx b/src/learning-sequence/sequence/UnitButton.jsx
new file mode 100644
index 00000000..36bf059d
--- /dev/null
+++ b/src/learning-sequence/sequence/UnitButton.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Button } from '@edx/paragon';
+import UnitIcon from './UnitIcon';
+import CompleteIcon from './CompleteIcon';
+
+export default function UnitButton({
+ onClick,
+ title,
+ type,
+ isActive,
+ isComplete,
+}) {
+ return (
+
+ )
+}
diff --git a/src/learning-sequence/sequence/UnitIcon.jsx b/src/learning-sequence/sequence/UnitIcon.jsx
new file mode 100644
index 00000000..5568e6ff
--- /dev/null
+++ b/src/learning-sequence/sequence/UnitIcon.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faFilm, faBook, faPencilAlt, faTasks, faLock } from '@fortawesome/free-solid-svg-icons';
+
+export default function UnitIcon({ type }) {
+ let icon = null;
+ switch (type) {
+ case 'video':
+ icon = faFilm;
+ break;
+ case 'other':
+ icon = faBook;
+ break;
+ case 'vertical':
+ icon = faTasks;
+ break;
+ case 'problem':
+ icon = faPencilAlt;
+ break;
+ case 'lock':
+ icon = faLock;
+ break;
+ default:
+ icon = faBook;
+ }
+
+ return (
+
+ );
+}
+
+UnitIcon.propTypes = {
+ type: PropTypes.oneOf(['video', 'other', 'vertical', 'problem', 'lock']).isRequired,
+};
diff --git a/src/learning-sequence/sequence/api.js b/src/learning-sequence/sequence/api.js
new file mode 100644
index 00000000..f8bc5f44
--- /dev/null
+++ b/src/learning-sequence/sequence/api.js
@@ -0,0 +1,50 @@
+import { getConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+
+/* eslint-disable import/prefer-default-export */
+
+const getSequenceXModuleHandlerUrl = (courseId, sequenceId) =>
+ `${getConfig().LMS_BASE_URL}/courses/${courseId}/xblock/${sequenceId}/handler/xmodule_handler`;
+
+
+export async function saveSequencePosition(courseId, sequenceId, position) {
+ // Post data sent to this endpoint must be url encoded
+ // TODO: Remove the need for this to be the case.
+ // TODO: Ensure this usage of URLSearchParams is working in Internet Explorer
+ const urlEncoded = new URLSearchParams();
+ urlEncoded.append('position', position + 1);
+ const requestConfig = {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ };
+
+ const { data } = await getAuthenticatedHttpClient().post(
+ `${getSequenceXModuleHandlerUrl(courseId, sequenceId)}/goto_position`,
+ urlEncoded.toString(),
+ requestConfig,
+ );
+
+ return data;
+}
+
+export async function getBlockCompletion(courseId, sequenceId, usageKey) {
+ // Post data sent to this endpoint must be url encoded
+ // TODO: Remove the need for this to be the case.
+ // TODO: Ensure this usage of URLSearchParams is working in Internet Explorer
+ const urlEncoded = new URLSearchParams();
+ urlEncoded.append('usage_key', usageKey);
+ const requestConfig = {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ };
+
+ const { data } = await getAuthenticatedHttpClient().post(
+ `${getSequenceXModuleHandlerUrl(courseId, sequenceId)}/get_completion`,
+ urlEncoded.toString(),
+ requestConfig,
+ );
+
+ if (data.complete) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/learning-sequence/sequence/content-lock/ContentLock.jsx b/src/learning-sequence/sequence/content-lock/ContentLock.jsx
new file mode 100644
index 00000000..dda8c7f4
--- /dev/null
+++ b/src/learning-sequence/sequence/content-lock/ContentLock.jsx
@@ -0,0 +1,37 @@
+import React, { useContext, useCallback } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faLock } from '@fortawesome/free-solid-svg-icons';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { history } from '@edx/frontend-platform';
+import { Button } from '@edx/paragon';
+
+import messages from './messages';
+
+function ContentLock({ intl, courseId, prereqSectionName, prereqId, sectionName }) {
+ const handleClick = useCallback(() => {
+ history.push(`/course/${courseId}/${prereqId}`);
+ });
+
+ return (
+ <>
+
+ {' '}
+ {sectionName}
+
+ {intl.formatMessage(messages['learn.contentLock.content.locked'])}
+ {intl.formatMessage(messages['learn.contentLock.complete.prerequisite'], {
+ prereqSectionName,
+ })}
+
+
+
+
+ >
+ );
+}
+
+ContentLock.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(ContentLock);
diff --git a/src/learning-sequence/sequence/content-lock/index.js b/src/learning-sequence/sequence/content-lock/index.js
new file mode 100644
index 00000000..81524e63
--- /dev/null
+++ b/src/learning-sequence/sequence/content-lock/index.js
@@ -0,0 +1 @@
+export { default } from './ContentLock';
diff --git a/src/learning-sequence/sequence/content-lock/messages.js b/src/learning-sequence/sequence/content-lock/messages.js
new file mode 100644
index 00000000..872009b1
--- /dev/null
+++ b/src/learning-sequence/sequence/content-lock/messages.js
@@ -0,0 +1,21 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ 'learn.contentLock.content.locked': {
+ id: 'learn.contentLock.content.locked',
+ defaultMessage: 'Content Locked',
+ description: 'Message shown to indicate that a piece of content is unavailable and has a prerequisite.',
+ },
+ 'learn.contentLock.complete.prerequisite': {
+ id: 'learn.contentLock.complete.prerequisite',
+ 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': {
+ id: 'learn.contentLock.goToSection',
+ defaultMessage: 'Go To Prerequisite Section',
+ description: 'A button users can click that navigates their browser to the prerequisite of this section.',
+ },
+});
+
+export default messages;
diff --git a/src/learning-sequence/sequence/messages.js b/src/learning-sequence/sequence/messages.js
new file mode 100644
index 00000000..fc3007bd
--- /dev/null
+++ b/src/learning-sequence/sequence/messages.js
@@ -0,0 +1,11 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ 'learn.loading.content.lock': {
+ id: 'learn.loading.content.lock',
+ defaultMessage: 'Loading locked content messaging...',
+ description: 'Message shown when an interface about locked content is being loaded',
+ },
+});
+
+export default messages;