Adding hard-coded application header.
This commit is contained in:
7
src/courseware/sequence/CompleteIcon.jsx
Normal file
7
src/courseware/sequence/CompleteIcon.jsx
Normal file
@@ -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 <FontAwesomeIcon icon={faCheckCircle} {...props} />;
|
||||
}
|
||||
155
src/courseware/sequence/Sequence.jsx
Normal file
155
src/courseware/sequence/Sequence.jsx
Normal file
@@ -0,0 +1,155 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import React, { useState, useEffect, Suspense } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Unit from './Unit';
|
||||
import SequenceNavigation from './SequenceNavigation';
|
||||
import PageLoading from '../PageLoading';
|
||||
import { getBlockCompletion, saveSequencePosition } from './api';
|
||||
import messages from './messages';
|
||||
import AlertList from '../../user-messages/AlertList';
|
||||
|
||||
const ContentLock = React.lazy(() => import('./content-lock'));
|
||||
|
||||
function Sequence({
|
||||
courseUsageKey,
|
||||
id,
|
||||
unitIds,
|
||||
units: initialUnits,
|
||||
displayName,
|
||||
showCompletion,
|
||||
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,
|
||||
}));
|
||||
|
||||
// TODO: Use callback
|
||||
const updateUnitCompletion = (unitId) => {
|
||||
// If the unit is already complete, don't check.
|
||||
if (units[unitId].complete) {
|
||||
return;
|
||||
}
|
||||
|
||||
getBlockCompletion(courseUsageKey, 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);
|
||||
if (onNavigateUnit !== null) {
|
||||
onNavigateUnit(newUnitId, units[newUnitId]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (savePosition) {
|
||||
saveSequencePosition(courseUsageKey, id, activeUnitIndex);
|
||||
}
|
||||
}, [activeUnitId]);
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column flex-grow-1">
|
||||
<div className="container-fluid">
|
||||
<AlertList topic="sequence" className="mt-3" />
|
||||
<SequenceNavigation
|
||||
onNext={handleNext}
|
||||
onNavigate={handleNavigate}
|
||||
onPrevious={handlePrevious}
|
||||
units={unitsArr}
|
||||
isLocked={isGated}
|
||||
showCompletion={showCompletion}
|
||||
/>
|
||||
</div>
|
||||
{isGated ? (
|
||||
<Suspense fallback={<PageLoading
|
||||
srMessage={intl.formatMessage(messages['learn.loading.content.lock'])}
|
||||
/>}
|
||||
>
|
||||
<ContentLock
|
||||
courseUsageKey={courseUsageKey}
|
||||
sectionName={displayName}
|
||||
prereqSectionName={prerequisite.name}
|
||||
prereqId={prerequisite.id}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<Unit key={activeUnitId} {...activeUnit} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Sequence.propTypes = {
|
||||
activeUnitId: PropTypes.string.isRequired,
|
||||
bannerText: PropTypes.string,
|
||||
courseUsageKey: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
isGated: PropTypes.bool.isRequired,
|
||||
isTimeLimited: PropTypes.bool.isRequired,
|
||||
onNavigateUnit: PropTypes.func,
|
||||
onNext: PropTypes.func.isRequired,
|
||||
onPrevious: PropTypes.func.isRequired,
|
||||
savePosition: PropTypes.bool.isRequired,
|
||||
showCompletion: PropTypes.bool.isRequired,
|
||||
prerequisite: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
units: PropTypes.objectOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
complete: PropTypes.bool,
|
||||
pageTitle: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
};
|
||||
|
||||
Sequence.defaultProps = {
|
||||
bannerText: null,
|
||||
onNavigateUnit: null,
|
||||
};
|
||||
|
||||
export default injectIntl(Sequence);
|
||||
35
src/courseware/sequence/SequenceNavigation.jsx
Normal file
35
src/courseware/sequence/SequenceNavigation.jsx
Normal file
@@ -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) => (
|
||||
<UnitButton
|
||||
key={unit.id}
|
||||
{...unit}
|
||||
isComplete={showCompletion && unit.complete}
|
||||
onClick={onNavigate.bind(null, index)}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<nav className="flex-grow-0 d-flex w-100 btn-group">
|
||||
<Button className="btn-outline-primary" onClick={onPrevious}>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
{isLocked ? <UnitButton type="lock" isActive /> : unitButtons}
|
||||
|
||||
<Button className="btn-outline-primary" onClick={onNext}>
|
||||
Next
|
||||
</Button>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
22
src/courseware/sequence/Unit.jsx
Normal file
22
src/courseware/sequence/Unit.jsx
Normal file
@@ -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, pageTitle }) {
|
||||
const iframeRef = useRef(null);
|
||||
const iframeUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}`;
|
||||
|
||||
return (
|
||||
<iframe
|
||||
className="flex-grow-1"
|
||||
title={pageTitle}
|
||||
ref={iframeRef}
|
||||
src={iframeUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Unit.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
pageTitle: PropTypes.string.isRequired,
|
||||
};
|
||||
30
src/courseware/sequence/UnitButton.jsx
Normal file
30
src/courseware/sequence/UnitButton.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
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,
|
||||
pageTitle,
|
||||
type,
|
||||
isActive,
|
||||
isComplete,
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
className={classNames({
|
||||
active: isActive,
|
||||
'btn-outline-primary': !isActive,
|
||||
'btn-outline-secondary': isActive,
|
||||
})}
|
||||
disabled={isActive}
|
||||
onClick={onClick}
|
||||
title={pageTitle}
|
||||
>
|
||||
<UnitIcon type={type} />
|
||||
{isComplete ? <CompleteIcon className="text-success ml-2" /> : null}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
35
src/courseware/sequence/UnitIcon.jsx
Normal file
35
src/courseware/sequence/UnitIcon.jsx
Normal file
@@ -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 (
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
);
|
||||
}
|
||||
|
||||
UnitIcon.propTypes = {
|
||||
type: PropTypes.oneOf(['video', 'other', 'vertical', 'problem', 'lock']).isRequired,
|
||||
};
|
||||
50
src/courseware/sequence/api.js
Normal file
50
src/courseware/sequence/api.js
Normal file
@@ -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 = (courseUsageKey, sequenceId) =>
|
||||
`${getConfig().LMS_BASE_URL}/courses/${courseUsageKey}/xblock/${sequenceId}/handler/xmodule_handler`;
|
||||
|
||||
|
||||
export async function saveSequencePosition(courseUsageKey, 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(courseUsageKey, sequenceId)}/goto_position`,
|
||||
urlEncoded.toString(),
|
||||
requestConfig,
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getBlockCompletion(courseUsageKey, 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(courseUsageKey, sequenceId)}/get_completion`,
|
||||
urlEncoded.toString(),
|
||||
requestConfig,
|
||||
);
|
||||
|
||||
if (data.complete) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
37
src/courseware/sequence/content-lock/ContentLock.jsx
Normal file
37
src/courseware/sequence/content-lock/ContentLock.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { 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, courseUsageKey, prereqSectionName, prereqId, sectionName,
|
||||
}) {
|
||||
const handleClick = useCallback(() => {
|
||||
history.push(`/course/${courseUsageKey}/${prereqId}`);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<FontAwesomeIcon icon={faLock} />{' '}
|
||||
{sectionName}
|
||||
</h3>
|
||||
<h4>{intl.formatMessage(messages['learn.contentLock.content.locked'])}</h4>
|
||||
<p>{intl.formatMessage(messages['learn.contentLock.complete.prerequisite'], {
|
||||
prereqSectionName,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
<Button className="btn-primary" onClick={handleClick}>{intl.formatMessage(messages['learn.contentLock.goToSection'])}</Button>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
ContentLock.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
export default injectIntl(ContentLock);
|
||||
1
src/courseware/sequence/content-lock/index.js
Normal file
1
src/courseware/sequence/content-lock/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './ContentLock';
|
||||
21
src/courseware/sequence/content-lock/messages.js
Normal file
21
src/courseware/sequence/content-lock/messages.js
Normal file
@@ -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;
|
||||
11
src/courseware/sequence/messages.js
Normal file
11
src/courseware/sequence/messages.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user