* upgrade paragon * mmp2p experiment code * mmp2p courseware triggers * mmp2p course-home triggers * mmp2p load styles * mmp2p - add missed locator docstrings * mmp2p test fixes * add lazy loading for image-bearing components * mmp2p experiment README * mmp2p add lazy loading for sidecard * generalize prices for currency options * mmp2p fix flyover mobile args * mmp2p fix lock paywall border display * mmp2 - add safety-rail around verifiedmode access
436 lines
14 KiB
JavaScript
436 lines
14 KiB
JavaScript
/* eslint-disable no-use-before-define */
|
|
import React, {
|
|
useEffect, useContext, useState,
|
|
} from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import {
|
|
sendTrackEvent,
|
|
sendTrackingLogEvent,
|
|
} from '@edx/frontend-platform/analytics';
|
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
import { useSelector } from 'react-redux';
|
|
import { history } from '@edx/frontend-platform';
|
|
|
|
// These should be reverted after the REV1512 experiment
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
|
|
|
|
import PageLoading from '../../../generic/PageLoading';
|
|
import { UserMessagesContext, ALERT_TYPES } from '../../../generic/user-messages';
|
|
import { useModel } from '../../../generic/model-store';
|
|
|
|
import CourseLicense from '../course-license';
|
|
import messages from './messages';
|
|
import { SequenceNavigation, UnitNavigation } from './sequence-navigation';
|
|
import SequenceContent from './SequenceContent';
|
|
|
|
/** [MM-P2P] Experiment */
|
|
import { MMP2PFlyover, MMP2PFlyoverMobile } from '../../../experiments/mm-p2p';
|
|
|
|
function REV1512Flyover({ toggleREV1512Flyover }) {
|
|
// This component should be reverted after the REV1512 experiment
|
|
return (
|
|
<div
|
|
className="rev-1512-box"
|
|
style={{
|
|
border: 'solid 1px #e1dddb',
|
|
height: '393px',
|
|
width: '330px',
|
|
verticalAlign: 'top',
|
|
marginLeft: '20px',
|
|
padding: '0 20px 20px 20px',
|
|
}}
|
|
>
|
|
<div
|
|
className="rev-1512-notification-div"
|
|
style={{
|
|
margin: '0 -20px 15px',
|
|
padding: '9px 20px 0',
|
|
fontSize: '16px',
|
|
}}
|
|
>
|
|
<span className="rev-1512-notification-span">Notifications</span>
|
|
<span
|
|
onClick={() => {
|
|
toggleREV1512Flyover();
|
|
}}
|
|
className="hideFlyover"
|
|
onKeyPress={(event) => {
|
|
if (event.key === 'Enter') {
|
|
toggleREV1512Flyover();
|
|
}
|
|
}}
|
|
role="button"
|
|
tabIndex={0}
|
|
>
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 14 14"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
style={{
|
|
float: 'right',
|
|
marginTop: '5.5px',
|
|
}}
|
|
>
|
|
<path d="M9.60625 7L13.5152 3.09102C13.9949 2.61133 13.9949 1.83359 13.5152 1.35352L12.6465 0.484766C12.1668 0.00507814 11.3891 0.00507814 10.909 0.484766L7 4.39375L3.09102 0.484766C2.61133 0.00507814 1.83359 0.00507814 1.35352 0.484766L0.484766 1.35352C0.00507814 1.8332 0.00507814 2.61094 0.484766 3.09102L4.39375 7L0.484766 10.909C0.00507814 11.3887 0.00507814 12.1664 0.484766 12.6465L1.35352 13.5152C1.8332 13.9949 2.61133 13.9949 3.09102 13.5152L7 9.60625L10.909 13.5152C11.3887 13.9949 12.1668 13.9949 12.6465 13.5152L13.5152 12.6465C13.9949 12.1668 13.9949 11.3891 13.5152 10.909L9.60625 7Z" fill="black" />
|
|
</svg>
|
|
</span>
|
|
<div
|
|
className="rev-1512-notification-block"
|
|
style={{
|
|
height: '9px',
|
|
background: '#F9F9F9',
|
|
margin: '7px -20px 0',
|
|
borderTop: '1px solid rgb(225, 221, 219)',
|
|
borderBottom: '1px solid rgb(225, 221, 219)',
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="rev-1512-optimizely-flyover-container" />
|
|
</div>
|
|
);
|
|
}
|
|
REV1512Flyover.propTypes = {
|
|
toggleREV1512Flyover: PropTypes.func.isRequired,
|
|
};
|
|
|
|
function REV1512FlyoverMobile({ toggleREV1512Flyover }) {
|
|
// This component should be reverted after the REV1512 experiment
|
|
return (
|
|
<div
|
|
className="rev-1512-box"
|
|
style={{
|
|
verticalAlign: 'top',
|
|
padding: '0 20px 20px 20px',
|
|
position: 'fixed',
|
|
backgroundColor: 'white',
|
|
zIndex: 1,
|
|
height: '100%',
|
|
width: '100%',
|
|
top: 0,
|
|
left: 0,
|
|
}}
|
|
>
|
|
<div
|
|
className="rev-1512-mobile-return-div"
|
|
style={{
|
|
margin: '0 -20px',
|
|
padding: '9px 20px 15px',
|
|
fontSize: '16px',
|
|
borderBottom: '1px solid rgb(225, 221, 219)',
|
|
}}
|
|
>
|
|
<span
|
|
className="rev-1512-mobile-return-span"
|
|
onClick={toggleREV1512Flyover}
|
|
onKeyPress={(event) => {
|
|
if (event.key === 'Enter') {
|
|
toggleREV1512Flyover();
|
|
}
|
|
}}
|
|
role="button"
|
|
tabIndex={0}
|
|
style={{
|
|
color: '#00262B',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
<FontAwesomeIcon
|
|
icon={faChevronLeft}
|
|
className="mr-2 fa-lg"
|
|
style={{
|
|
marginBottom: 2,
|
|
}}
|
|
/>
|
|
<span className="rev-1512-mobile-return-text">
|
|
Back to course
|
|
</span>
|
|
</span>
|
|
</div>
|
|
<div
|
|
className="rev-1512-notification-div"
|
|
style={{
|
|
margin: '0 -20px 15px',
|
|
padding: '9px 20px 0',
|
|
fontSize: '16px',
|
|
}}
|
|
>
|
|
<span
|
|
className="rev-1512-notification-span"
|
|
style={{
|
|
color: '#00262B',
|
|
}}
|
|
>
|
|
Notifications
|
|
</span>
|
|
<div
|
|
className="rev-1512-notification-block"
|
|
style={{
|
|
height: '9px',
|
|
background: '#F9F9F9',
|
|
margin: '7px -20px 0',
|
|
borderTop: '1px solid rgb(225, 221, 219)',
|
|
borderBottom: '1px solid rgb(225, 221, 219)',
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="rev-1512-optimizely-flyover-container" />
|
|
</div>
|
|
);
|
|
}
|
|
REV1512FlyoverMobile.propTypes = {
|
|
toggleREV1512Flyover: PropTypes.func.isRequired,
|
|
};
|
|
|
|
function Sequence({
|
|
unitId,
|
|
sequenceId,
|
|
courseId,
|
|
unitNavigationHandler,
|
|
nextSequenceHandler,
|
|
previousSequenceHandler,
|
|
intl,
|
|
isREV1512FlyoverVisible, /* This line should be reverted after the REV1512 experiment */
|
|
REV1512FlyoverEnabled, /* This line should be reverted after the REV1512 experiment */
|
|
toggleREV1512Flyover, /* This line should be reverted after the REV1512 experiment */
|
|
/** [MM-P2P] Experiment */
|
|
mmp2p,
|
|
}) {
|
|
const course = useModel('coursewareMeta', courseId);
|
|
const sequence = useModel('sequences', sequenceId);
|
|
const unit = useModel('units', unitId);
|
|
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
|
const handleNext = () => {
|
|
const nextIndex = sequence.unitIds.indexOf(unitId) + 1;
|
|
if (nextIndex < sequence.unitIds.length) {
|
|
const newUnitId = sequence.unitIds[nextIndex];
|
|
handleNavigate(newUnitId);
|
|
} else {
|
|
nextSequenceHandler();
|
|
}
|
|
};
|
|
|
|
// These should be reverted after the REV1512 experiment:
|
|
const userAgent = typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
|
|
const isMobile = Boolean(
|
|
userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i),
|
|
);
|
|
|
|
const handlePrevious = () => {
|
|
const previousIndex = sequence.unitIds.indexOf(unitId) - 1;
|
|
if (previousIndex >= 0) {
|
|
const newUnitId = sequence.unitIds[previousIndex];
|
|
handleNavigate(newUnitId);
|
|
} else {
|
|
previousSequenceHandler();
|
|
}
|
|
};
|
|
|
|
const handleNavigate = (destinationUnitId) => {
|
|
unitNavigationHandler(destinationUnitId);
|
|
};
|
|
|
|
const logEvent = (eventName, widgetPlacement, targetUnitId) => {
|
|
// Note: tabs are tracked with a 1-indexed position
|
|
// as opposed to a 0-index used throughout this MFE
|
|
const currentIndex = sequence.unitIds.length > 0 ? sequence.unitIds.indexOf(unitId) : 0;
|
|
const payload = {
|
|
current_tab: currentIndex + 1,
|
|
id: unitId,
|
|
tab_count: sequence.unitIds.length,
|
|
widget_placement: widgetPlacement,
|
|
};
|
|
if (targetUnitId) {
|
|
const targetIndex = sequence.unitIds.indexOf(targetUnitId);
|
|
payload.target_tab = targetIndex + 1;
|
|
}
|
|
sendTrackEvent(eventName, payload);
|
|
sendTrackingLogEvent(eventName, payload);
|
|
};
|
|
|
|
const { add, remove } = useContext(UserMessagesContext);
|
|
useEffect(() => {
|
|
let id = null;
|
|
if (sequenceStatus === 'loaded') {
|
|
if (sequence.bannerText) {
|
|
id = add({
|
|
code: null,
|
|
dismissible: false,
|
|
text: sequence.bannerText,
|
|
type: ALERT_TYPES.INFO,
|
|
topic: 'sequence',
|
|
});
|
|
}
|
|
}
|
|
return () => {
|
|
if (id) {
|
|
remove(id);
|
|
}
|
|
};
|
|
}, [sequenceStatus, sequence]);
|
|
|
|
const [unitHasLoaded, setUnitHasLoaded] = useState(false);
|
|
const handleUnitLoaded = () => {
|
|
setUnitHasLoaded(true);
|
|
};
|
|
useEffect(() => {
|
|
if (unit) {
|
|
setUnitHasLoaded(false);
|
|
}
|
|
}, [unit]);
|
|
|
|
if (sequenceStatus === 'loading') {
|
|
if (!sequenceId) {
|
|
return (<div> {intl.formatMessage(messages['learn.sequence.no.content'])} </div>);
|
|
}
|
|
return (
|
|
<PageLoading
|
|
srMessage={intl.formatMessage(messages['learn.loading.learning.sequence'])}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/*
|
|
TODO: When the micro-frontend supports viewing special exams without redirecting to the legacy
|
|
experience, we can remove this whole conditional. For now, though, we show the spinner here
|
|
because we expect CoursewareContainer to be performing a redirect to the legacy experience while
|
|
we're waiting. That redirect may take a few seconds, so we show the spinner in the meantime.
|
|
*/
|
|
if (sequenceStatus === 'loaded' && sequence.isTimeLimited) {
|
|
return (
|
|
<PageLoading
|
|
srMessage={intl.formatMessage(messages['learn.loading.learning.sequence'])}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const gated = sequence && sequence.gatedContent !== undefined && sequence.gatedContent.gated;
|
|
const goToCourseExitPage = () => {
|
|
history.push(`/course/${courseId}/course-end`);
|
|
};
|
|
|
|
if (sequenceStatus === 'loaded') {
|
|
return (
|
|
<div>
|
|
<div className="sequence-container" style={{ display: 'inline-flex', flexDirection: 'row' }}>
|
|
<div className="sequence" style={{ width: '100%' }}>
|
|
<SequenceNavigation
|
|
sequenceId={sequenceId}
|
|
unitId={unitId}
|
|
className="mb-4"
|
|
toggleREV1512Flyover={toggleREV1512Flyover} /* This line should be reverted after REV1512 experiment */
|
|
REV1512FlyoverEnabled={REV1512FlyoverEnabled} /* This line should be reverted after REV1512 experiment */
|
|
isREV1512FlyoverVisible={isREV1512FlyoverVisible} /* should be reverted after REV1512 experiment */
|
|
/** [MM-P2P] Experiment */
|
|
mmp2p={mmp2p}
|
|
|
|
nextSequenceHandler={() => {
|
|
logEvent('edx.ui.lms.sequence.next_selected', 'top');
|
|
handleNext();
|
|
}}
|
|
onNavigate={(destinationUnitId) => {
|
|
logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
|
|
handleNavigate(destinationUnitId);
|
|
}}
|
|
previousSequenceHandler={() => {
|
|
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
|
|
handlePrevious();
|
|
}}
|
|
goToCourseExitPage={() => goToCourseExitPage()}
|
|
/>
|
|
<div className="unit-container flex-grow-1">
|
|
<SequenceContent
|
|
courseId={courseId}
|
|
gated={gated}
|
|
sequenceId={sequenceId}
|
|
unitId={unitId}
|
|
unitLoadedHandler={handleUnitLoaded}
|
|
/** [MM-P2P] Experiment */
|
|
mmp2p={mmp2p}
|
|
/>
|
|
{unitHasLoaded && (
|
|
<UnitNavigation
|
|
sequenceId={sequenceId}
|
|
unitId={unitId}
|
|
onClickPrevious={() => {
|
|
logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
|
|
handlePrevious();
|
|
}}
|
|
onClickNext={() => {
|
|
logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
|
|
handleNext();
|
|
}}
|
|
goToCourseExitPage={() => goToCourseExitPage()}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{/* This block of code should be reverted post REV1512 experiment */}
|
|
{/** [MM-P2P] Experiment (additional conditional) */}
|
|
{!mmp2p.state.isEnabled && REV1512FlyoverEnabled && isREV1512FlyoverVisible() && (
|
|
isMobile
|
|
? <REV1512FlyoverMobile toggleREV1512Flyover={toggleREV1512Flyover} />
|
|
: <REV1512Flyover toggleREV1512Flyover={toggleREV1512Flyover} />
|
|
)}
|
|
{/** [MM-P2P] Experiment */}
|
|
{(mmp2p.state.isEnabled && mmp2p.flyover.isVisible) && (
|
|
isMobile
|
|
? <MMP2PFlyoverMobile options={mmp2p} />
|
|
: <MMP2PFlyover options={mmp2p} />
|
|
)}
|
|
</div>
|
|
<CourseLicense license={course.license || undefined} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// sequence status 'failed' and any other unexpected sequence status.
|
|
return (
|
|
<p className="text-center py-5 mx-auto" style={{ maxWidth: '30em' }}>
|
|
{intl.formatMessage(messages['learn.course.load.failure'])}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
Sequence.propTypes = {
|
|
unitId: PropTypes.string,
|
|
sequenceId: PropTypes.string,
|
|
courseId: PropTypes.string.isRequired,
|
|
unitNavigationHandler: PropTypes.func.isRequired,
|
|
nextSequenceHandler: PropTypes.func.isRequired,
|
|
previousSequenceHandler: PropTypes.func.isRequired,
|
|
intl: intlShape.isRequired,
|
|
toggleREV1512Flyover: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */
|
|
isREV1512FlyoverVisible: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */
|
|
REV1512FlyoverEnabled: PropTypes.bool.isRequired, /* This line should be reverted after the REV1512 experiment */
|
|
|
|
/** [MM-P2P] Experiment */
|
|
mmp2p: PropTypes.shape({
|
|
flyover: PropTypes.shape({
|
|
isVisible: PropTypes.bool.isRequired,
|
|
}),
|
|
meta: PropTypes.shape({
|
|
showLock: PropTypes.bool,
|
|
}),
|
|
state: PropTypes.shape({
|
|
isEnabled: PropTypes.bool.isRequired,
|
|
}),
|
|
}),
|
|
};
|
|
|
|
Sequence.defaultProps = {
|
|
sequenceId: null,
|
|
unitId: null,
|
|
|
|
/** [MM-P2P] Experiment */
|
|
mmp2p: {
|
|
flyover: { isVisible: false },
|
|
meta: { showLock: false },
|
|
state: { isEnabled: false },
|
|
},
|
|
};
|
|
|
|
export default injectIntl(Sequence);
|