diff --git a/src/learning-sequence/LearningSequencePage.jsx b/src/learning-sequence/LearningSequencePage.jsx
index 408430f0..fdd5f571 100644
--- a/src/learning-sequence/LearningSequencePage.jsx
+++ b/src/learning-sequence/LearningSequencePage.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useContext } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -6,7 +6,7 @@ import PageLoading from './PageLoading';
import messages from './messages';
import CourseBreadcrumbs from './CourseBreadcrumbs';
-// import SubSection from './SubSection';
+import SubSection from './SubSection';
import { useCourseStructure } from './hooks';
import CourseStructureContext from './CourseStructureContext';
@@ -37,7 +37,7 @@ function LearningSequencePage({ match, intl }) {
/>}
{loaded && }
- {/* */}
+
diff --git a/src/learning-sequence/SubSection.jsx b/src/learning-sequence/SubSection.jsx
new file mode 100644
index 00000000..abe455ef
--- /dev/null
+++ b/src/learning-sequence/SubSection.jsx
@@ -0,0 +1,239 @@
+import React, { Component, useContext, useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import { getConfig } from '@edx/frontend-platform';
+
+import SubSectionNavigation from './SubSectionNavigation';
+import { getSubSectionMetadata } from './api';
+import CourseStructureContext from './CourseStructureContext';
+import Unit from './Unit';
+
+function useSubSectionMetadata(courseId, subSectionId) {
+ const [metadata, setMetadata] = useState(null);
+ const [loaded, setLoaded] = useState(false);
+
+ useEffect(() => {
+ setLoaded(false);
+ getSubSectionMetadata(courseId, subSectionId).then((data) => {
+ setMetadata(data);
+ setLoaded(true);
+ });
+ }, [courseId, subSectionId]);
+
+ return {
+ metadata,
+ loaded,
+ };
+}
+
+function useExamRedirect(metadata, blocks) {
+ useEffect(() => {
+ if (metadata !== null && blocks !== null) {
+ if (metadata.isTimeLimited) {
+ global.location.href = blocks[metadata.itemId].lmsWebUrl;
+ }
+ }
+ }, [metadata, blocks]);
+}
+
+/*
+
+
+ const calculateUnitId = (metadata, options) => {
+ const { first, last, preferredUnitId } = options;
+ let position = metadata.position - 1; // metadata's position is 1's indexed
+ position = first ? 0 : position;
+ position = last ? metadata.unitIds.length - 1 : position;
+ position = preferredUnitId ? metadata.unitIds.indexOf(preferredUnitId) : position;
+ const unitId = metadata.items[position].id;
+
+ return unitId;
+ }
+
+ handleUnitChange = (unitId) => {
+ this.setState({
+ unitId,
+ });
+ }
+
+ */
+
+export default function SubSection() {
+ const {
+ courseId,
+ subSectionId,
+ unitId,
+ blocks,
+ } = useContext(CourseStructureContext);
+ const { metadata } = useSubSectionMetadata(courseId, subSectionId);
+
+ useExamRedirect(metadata, blocks);
+
+ if (blocks === null || metadata === null) {
+ return null;
+ }
+
+ const unit = blocks[unitId];
+
+ // units={this.state.units}
+ // unitIds={this.state.subSectionMetadata.unitIds}
+ // activeUnitId={this.state.unitId}
+ // unitClickHandler={this.handleUnitChange}
+ // nextClickHandler={this.handleNextClick}
+ // previousClickHandler={this.handlePreviousClick}
+ return (
+
+ );
+}
+
+
+/*
+
+
{course.displayName}
+
+
+
+
+
+*/
+
+// constructor(props, context) {
+// super(props, context);
+
+// // this.state = {
+// // loading: true,
+// // blocks: {},
+// // units: {},
+// // subSectionMetadata: null,
+// // subSectionId: null,
+// // subSectionIds: [],
+// // unitId: null,
+// // courseBlockId: null,
+// // };
+
+// // this.iframeRef = React.createRef();
+// }
+
+// componentDidMount() {
+// loadCourseSequence(this.props.match.params.courseId, this.props.match.params.subSectionId, this.props.match.params.unitId, this.context.authenticatedUser.username)
+// .then(({
+// blocks, courseBlockId, subSectionIds, subSectionMetadata, units, unitId,
+// }) => {
+// console.log(subSectionMetadata);
+// console.log(blocks[subSectionMetadata.itemId].lmsWebUrl);
+// // If the sub section is time limited, that means it is some sort of special exam.
+// const specialExam = subSectionMetadata.isTimeLimited;
+// if (specialExam) {
+// global.location.href = blocks[subSectionMetadata.itemId].lmsWebUrl;
+// return; // We get out of here to abort loading.
+// }
+
+// this.setState({
+// loading: false,
+// blocks,
+// units,
+// subSectionMetadata,
+// subSectionId: subSectionMetadata.itemId,
+// subSectionIds,
+// unitId,
+// // eslint-disable-next-line react/no-unused-state
+// courseBlockId, // TODO: Currently unused, but may be necessary.
+// });
+// });
+// }
+
+// componentDidUpdate(prevProps, prevState) {
+// if (
+// this.props.match.params.courseId !== prevProps.match.params.courseId ||
+// this.state.subSectionId !== prevState.subSectionId ||
+// this.state.unitId !== prevState.unitId
+// ) {
+// history.push(`/course/${this.props.match.params.courseId}/${this.state.subSectionId}/${this.state.unitId}`);
+// }
+// }
+
+
+// handlePreviousClick = () => {
+// const index = this.state.subSectionMetadata.unitIds.indexOf(this.state.unitId);
+// if (index > 0) {
+// this.setState({
+// unitId: this.state.subSectionMetadata.unitIds[index - 1],
+// });
+// } else {
+// const subSectionIndex = this.state.subSectionIds.indexOf(this.state.subSectionId);
+// if (subSectionIndex > 0) {
+// const previousSubSectionId = this.state.subSectionIds[subSectionIndex - 1];
+
+// loadSubSectionMetadata(this.props.match.params.courseId, previousSubSectionId, { last: true }).then(({ subSectionMetadata, units, unitId }) => {
+// const specialExam = subSectionMetadata.isTimeLimited;
+// if (specialExam) {
+// global.location.href = this.state.blocks[subSectionMetadata.itemId].lmsWebUrl;
+// return; // We get out of here to abort loading.
+// }
+// this.setState({
+// subSectionId: subSectionMetadata.itemId,
+// subSectionMetadata,
+// units,
+// unitId,
+// });
+// });
+// } else {
+// console.log('we are at the beginning!');
+// // TODO: We need to calculate whether we're on the first/last subSection in render so we can
+// // disable the Next/Previous buttons. That'll involve extracting a bit of logic from this
+// // function and handleNextClick below and reusing it - memoized, probably - in render().
+// }
+// }
+// }
+
+// handleNextClick = () => {
+// const index = this.state.subSectionMetadata.unitIds.indexOf(this.state.unitId);
+// if (index < this.state.subSectionMetadata.unitIds.length - 1) {
+// this.setState({
+// unitId: this.state.subSectionMetadata.unitIds[index + 1],
+// });
+// } else {
+// const subSectionIndex = this.state.subSectionIds.indexOf(this.state.subSectionId);
+// if (subSectionIndex < this.state.subSectionIds.length - 1) {
+// const nextSubSectionId = this.state.subSectionIds[subSectionIndex + 1];
+
+// loadSubSectionMetadata(this.props.match.params.courseId, nextSubSectionId, { first: true })
+// .then(({ subSectionMetadata, units, unitId }) => {
+// const specialExam = subSectionMetadata.isTimeLimited;
+// if (specialExam) {
+// global.location.href = this.state.blocks[subSectionMetadata.itemId].lmsWebUrl;
+// return; // We get out of here to abort loading.
+// }
+// this.setState({
+// subSectionId: subSectionMetadata.itemId,
+// subSectionMetadata,
+// units,
+// unitId,
+// });
+// });
+// } else {
+// console.log('we are at the end!');
+// }
+// }
+// }
diff --git a/src/learning-sequence/SubSectionNavigation.jsx b/src/learning-sequence/SubSectionNavigation.jsx
index 95f86b78..59d2bbb2 100644
--- a/src/learning-sequence/SubSectionNavigation.jsx
+++ b/src/learning-sequence/SubSectionNavigation.jsx
@@ -1,79 +1,105 @@
-import React, { Component } from 'react';
+import React, { Component, useCallback, useContext } from 'react';
import PropTypes from 'prop-types';
+import { history } from '@edx/frontend-platform';
import { Button } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFilm, faBook, faPencilAlt, faTasks } from '@fortawesome/free-solid-svg-icons';
-export default class SubSectionNavigation extends Component {
- renderUnitIcon(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;
- default:
- icon = faBook;
- }
- return ;
+import { useCurrentSubSection, useCurrentUnit, usePreviousUnit, useNextUnit, useCurrentCourse, useCurrentSubSectionUnits } from './hooks';
+import CourseStructureContext from './CourseStructureContext';
+
+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;
+ default:
+ icon = faBook;
}
- renderUnits() {
- return this.props.unitIds.map((id) => {
- const { type } = this.props.units[id];
- const disabled = this.props.activeUnitId === id;
- return (
-
- );
- });
- }
-
- render() {
- return (
-
- );
- }
+ return (
+
+ );
}
+export default function SubSectionNavigation() {
+ const { courseId } = useContext(CourseStructureContext);
+ const subSection = useCurrentSubSection();
+ const previousUnit = usePreviousUnit();
+ const nextUnit = useNextUnit();
+
+ const handlePreviousClick = useCallback(() => {
+ if (previousUnit) {
+ history.push(`/course/${courseId}/${subSection.id}/${previousUnit.id}`);
+ }
+ });
+ const handleNextClick = useCallback(() => {
+ if (nextUnit) {
+ history.push(`/course/${courseId}/${subSection.id}/${nextUnit.id}`);
+ }
+ });
+
+ return (
+
+ );
+}
+
+function UnitNavigation() {
+ const units = useCurrentSubSectionUnits();
+}
+
+function renderUnits() {
+ return this.props.unitIds.map((id) => {
+ const { type } = this.props.units[id];
+ const disabled = this.props.activeUnitId === id;
+ return (
+
+ );
+ });
+}
+
+
SubSectionNavigation.propTypes = {
- unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
- units: PropTypes.objectOf(PropTypes.shape({
- pageTitle: PropTypes.string.isRequired,
- type: PropTypes.oneOf(['video', 'other', 'vertical', 'problem']).isRequired,
- })).isRequired,
- activeUnitId: PropTypes.string.isRequired,
- unitClickHandler: PropTypes.func.isRequired,
- nextClickHandler: PropTypes.func.isRequired,
- previousClickHandler: PropTypes.func.isRequired,
+ // unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
+ // units: PropTypes.objectOf(PropTypes.shape({
+ // pageTitle: PropTypes.string.isRequired,
+ // type: PropTypes.oneOf(['video', 'other', 'vertical', 'problem']).isRequired,
+ // })).isRequired,
+ // activeUnitId: PropTypes.string.isRequired,
+ // unitClickHandler: PropTypes.func.isRequired,
+ // nextClickHandler: PropTypes.func.isRequired,
+ // previousClickHandler: PropTypes.func.isRequired,
};
diff --git a/src/learning-sequence/Unit.jsx b/src/learning-sequence/Unit.jsx
new file mode 100644
index 00000000..2936d070
--- /dev/null
+++ b/src/learning-sequence/Unit.jsx
@@ -0,0 +1,23 @@
+import React, { useRef } from 'react';
+import PropTypes from 'prop-types';
+import { getConfig } from '@edx/frontend-platform';
+
+export default function Unit({ id, unit }) {
+ const iframeRef = useRef(null);
+ const iframeUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}`;
+ const { displayName } = unit;
+ return (
+
+ );
+}
+
+Unit.propTypes = {
+ id: PropTypes.string.isRequired,
+ unit: PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ }).isRequired,
+};
diff --git a/src/learning-sequence/api.js b/src/learning-sequence/api.js
index 0cb57f07..5b40c2ba 100644
--- a/src/learning-sequence/api.js
+++ b/src/learning-sequence/api.js
@@ -60,7 +60,7 @@ export async function loadSubSectionMetadata(courseId, subSectionId, {
};
}
-async function getSubSectionMetadata(courseId, subSectionId) {
+export async function getSubSectionMetadata(courseId, subSectionId) {
const { data } = await getAuthenticatedHttpClient()
.get(`${getConfig().LMS_BASE_URL}/courses/${courseId}/xblock/${subSectionId}/handler/xmodule_handler/metadata`, {});
@@ -126,6 +126,20 @@ export function createSubSectionIdList(blocks, entryPointId, subSections = []) {
return subSections;
}
+export function createUnitIdList(blocks, entryPointId, units = []) {
+ const block = blocks[entryPointId];
+ if (block.type === 'vertical') {
+ units.push(block.id);
+ }
+ if (Array.isArray(block.children)) {
+ for (let i = 0; i < block.children.length; i++) {
+ const childId = block.children[i];
+ createUnitIdList(blocks, childId, units);
+ }
+ }
+ return units;
+}
+
export function findBlockAncestry(blocks, blockId, descendents = []) {
const block = blocks[blockId];
descendents.unshift(block);
diff --git a/src/learning-sequence/hooks.js b/src/learning-sequence/hooks.js
index 5d24dbf8..57ac3718 100644
--- a/src/learning-sequence/hooks.js
+++ b/src/learning-sequence/hooks.js
@@ -2,7 +2,7 @@ import { useContext, useMemo, useState, useEffect } from 'react';
import { AppContext } from '@edx/frontend-platform/react';
import CourseStructureContext from './CourseStructureContext';
-import { findBlockAncestry, getCourseBlocks, createBlocksMap } from './api';
+import { findBlockAncestry, getCourseBlocks, createBlocksMap, createSubSectionIdList, createUnitIdList } from './api';
export function useBlockAncestry(blockId) {
const { blocks, loaded } = useContext(CourseStructureContext);
@@ -20,7 +20,7 @@ export function useBlockAncestry(blockId) {
export function useCourseStructure(courseId) {
const { authenticatedUser } = useContext(AppContext);
- const [blocks, setBlocks] = useState({});
+ const [blocks, setBlocks] = useState(null);
const [loaded, setLoaded] = useState(false);
const [courseBlockId, setCourseBlockId] = useState();
@@ -37,3 +37,77 @@ export function useCourseStructure(courseId) {
blocks, loaded, courseBlockId,
};
}
+
+export function useCurrentCourse() {
+ const { loaded, courseBlockId, blocks } = useContext(CourseStructureContext);
+
+ return loaded ? blocks[courseBlockId] : null;
+}
+
+export function useCurrentSubSection() {
+ const { loaded, blocks, subSectionId } = useContext(CourseStructureContext);
+
+ return loaded ? blocks[subSectionId] : null;
+}
+
+export function useCurrentSection() {
+ const { loaded, blocks } = useContext(CourseStructureContext);
+ const subSection = useCurrentSubSection();
+ return loaded ? blocks[subSection.parentId] : null;
+}
+
+export function useCurrentUnit() {
+ const { loaded, blocks, unitId } = useContext(CourseStructureContext);
+
+ return loaded ? blocks[unitId] : null;
+}
+
+
+export function useUnitIds() {
+ const { loaded, blocks, courseBlockId } = useContext(CourseStructureContext);
+
+ return useMemo(
+ () => (loaded ? createUnitIdList(blocks, courseBlockId) : []),
+ [loaded, blocks, courseBlockId],
+ );
+}
+
+
+export function usePreviousUnit() {
+ const { loaded, blocks, unitId } = useContext(CourseStructureContext);
+ const unitIds = useUnitIds();
+
+ const currentUnitIndex = unitIds.indexOf(unitId);
+ if (currentUnitIndex === 0) {
+ return null;
+ }
+ return loaded ? blocks[unitIds[currentUnitIndex - 1]] : null;
+}
+
+export function useNextUnit() {
+ const { loaded, blocks, unitId } = useContext(CourseStructureContext);
+ const unitIds = useUnitIds();
+
+ const currentUnitIndex = unitIds.indexOf(unitId);
+ if (currentUnitIndex === unitIds.length - 1) {
+ return null;
+ }
+ return loaded ? blocks[unitIds[currentUnitIndex + 1]] : null;
+}
+
+export function useCurrentSubSectionUnits() {
+ const { blocks } = useContext(CourseStructureContext);
+ const subSection = useCurrentSubSection();
+ return subSection.children.map(id => blocks[id]);
+}
+
+export function useSubSectionIdList() {
+ const { loaded, blocks, courseBlockId } = useContext(CourseStructureContext);
+
+ const subSectionIdList = useMemo(
+ () => (loaded ? createSubSectionIdList(blocks, courseBlockId) : []),
+ [blocks, courseBlockId],
+ );
+
+ return subSectionIdList;
+}