Compare commits
1 Commits
dependabot
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bde0a80cf0 |
14
package-lock.json
generated
14
package-lock.json
generated
@@ -5771,22 +5771,22 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"codecov": {
|
"codecov": {
|
||||||
"version": "3.7.2",
|
"version": "3.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.1.tgz",
|
||||||
"integrity": "sha512-fmCjAkTese29DUX3GMIi4EaKGflHa4K51EoMc29g8fBHawdk/+KEq5CWOeXLdd9+AT7o1wO4DIpp/Z1KCqCz1g==",
|
"integrity": "sha512-Qm7ltx1pzLPsliZY81jyaQ80dcNR4/JpcX0IHCIWrHBXgseySqbdbYfkdiXd7o/xmzQpGRVCKGYeTrHUpn6Dcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"argv": "0.0.2",
|
"argv": "0.0.2",
|
||||||
"ignore-walk": "3.0.3",
|
"ignore-walk": "3.0.3",
|
||||||
"js-yaml": "3.13.1",
|
"js-yaml": "3.14.0",
|
||||||
"teeny-request": "6.0.1",
|
"teeny-request": "6.0.1",
|
||||||
"urlgrey": "0.4.4"
|
"urlgrey": "0.4.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-yaml": {
|
"js-yaml": {
|
||||||
"version": "3.13.1",
|
"version": "3.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
|
||||||
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
|
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"argparse": "^1.0.7",
|
"argparse": "^1.0.7",
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
"@testing-library/react": "10.3.0",
|
"@testing-library/react": "10.3.0",
|
||||||
"@testing-library/user-event": "12.0.17",
|
"@testing-library/user-event": "12.0.17",
|
||||||
"axios-mock-adapter": "1.18.2",
|
"axios-mock-adapter": "1.18.2",
|
||||||
"codecov": "3.7.2",
|
"codecov": "3.8.1",
|
||||||
"es-check": "5.1.4",
|
"es-check": "5.1.4",
|
||||||
"glob": "7.1.6",
|
"glob": "7.1.6",
|
||||||
"husky": "3.1.0",
|
"husky": "3.1.0",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
import './courseHomeMetadata.factory';
|
import './courseHomeMetadata.factory';
|
||||||
import './datesTabData.factory';
|
import './datesTabData.factory';
|
||||||
import './outlineTabData.factory';
|
import './outlineTabData.factory';
|
||||||
|
import './progressTabData.factory';
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
|
||||||
|
|
||||||
|
// Sample data helpful when developing & testing, to see a variety of configurations.
|
||||||
|
// This set of data may not be realistic, but it is intended to demonstrate many UI results.
|
||||||
|
Factory.define('progressTabData')
|
||||||
|
.attrs({
|
||||||
|
certificate_data: null,
|
||||||
|
completion_summary: {
|
||||||
|
complete_count: 1,
|
||||||
|
incomplete_count: 1,
|
||||||
|
locked_count: 0,
|
||||||
|
},
|
||||||
|
course_grade: {
|
||||||
|
percent: 0,
|
||||||
|
is_passing: false,
|
||||||
|
},
|
||||||
|
section_scores: [
|
||||||
|
{
|
||||||
|
display_name: 'First section',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Homework',
|
||||||
|
display_name: 'First subsection',
|
||||||
|
has_graded_assignment: true,
|
||||||
|
num_points_earned: 0,
|
||||||
|
num_points_possible: 1,
|
||||||
|
percent_graded: 0.0,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display_name: 'Second section',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Homework',
|
||||||
|
display_name: 'Second subsection',
|
||||||
|
has_graded_assignment: true,
|
||||||
|
num_points_earned: 1,
|
||||||
|
num_points_possible: 1,
|
||||||
|
percent_graded: 1.0,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
enrollment_mode: 'audit',
|
||||||
|
grading_policy: {
|
||||||
|
assignment_policies: [
|
||||||
|
{
|
||||||
|
num_droppable: 1,
|
||||||
|
short_label: 'HW',
|
||||||
|
type: 'Homework',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
grade_range: {
|
||||||
|
pass: 0.75,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
studio_url: 'http://studio.edx.org/settings/grading/course-v1:edX+Test+run',
|
||||||
|
verification_data: {
|
||||||
|
link: null,
|
||||||
|
status: 'none',
|
||||||
|
status_date: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -142,8 +142,11 @@ export async function getProgressTabData(courseId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProctoringInfoData(courseId) {
|
export async function getProctoringInfoData(courseId, username) {
|
||||||
const url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?course_id=${encodeURIComponent(courseId)}`;
|
let url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?course_id=${encodeURIComponent(courseId)}`;
|
||||||
|
if (username) {
|
||||||
|
url += `&username=${encodeURIComponent(username)}`;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ function OutlineTab({ intl }) {
|
|||||||
const {
|
const {
|
||||||
org,
|
org,
|
||||||
title,
|
title,
|
||||||
|
username,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -204,6 +205,7 @@ function OutlineTab({ intl }) {
|
|||||||
<div className="col col-12 col-md-4">
|
<div className="col col-12 col-md-4">
|
||||||
<ProctoringInfoPanel
|
<ProctoringInfoPanel
|
||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
|
username={username}
|
||||||
/>
|
/>
|
||||||
{courseGoalToDisplay && goalOptions && goalOptions.length > 0 && (
|
{courseGoalToDisplay && goalOptions && goalOptions.length > 0 && (
|
||||||
<UpdateGoalSelector
|
<UpdateGoalSelector
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ function SequenceLink({
|
|||||||
day="numeric"
|
day="numeric"
|
||||||
month="short"
|
month="short"
|
||||||
year="numeric"
|
year="numeric"
|
||||||
hour12={false}
|
|
||||||
timeZoneName="short"
|
timeZoneName="short"
|
||||||
value={due}
|
value={due}
|
||||||
{...timezoneFormatArgs}
|
{...timezoneFormatArgs}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ function CourseEndAlert({ payload }) {
|
|||||||
day="numeric"
|
day="numeric"
|
||||||
month="short"
|
month="short"
|
||||||
year="numeric"
|
year="numeric"
|
||||||
hour12={false}
|
|
||||||
timeZoneName="short"
|
timeZoneName="short"
|
||||||
value={endDate}
|
value={endDate}
|
||||||
{...timezoneFormatArgs}
|
{...timezoneFormatArgs}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ function CourseStartAlert({ payload }) {
|
|||||||
day="numeric"
|
day="numeric"
|
||||||
month="short"
|
month="short"
|
||||||
year="numeric"
|
year="numeric"
|
||||||
hour12={false}
|
|
||||||
timeZoneName="short"
|
timeZoneName="short"
|
||||||
value={startDate}
|
value={startDate}
|
||||||
{...timezoneFormatArgs}
|
{...timezoneFormatArgs}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Button } from '@edx/paragon';
|
|||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { getProctoringInfoData } from '../../data/api';
|
import { getProctoringInfoData } from '../../data/api';
|
||||||
|
|
||||||
function ProctoringInfoPanel({ courseId, intl }) {
|
function ProctoringInfoPanel({ courseId, username, intl }) {
|
||||||
const [status, setStatus] = useState('');
|
const [status, setStatus] = useState('');
|
||||||
const [link, setLink] = useState('');
|
const [link, setLink] = useState('');
|
||||||
const [releaseDate, setReleaseDate] = useState(null);
|
const [releaseDate, setReleaseDate] = useState(null);
|
||||||
@@ -74,7 +74,7 @@ function ProctoringInfoPanel({ courseId, intl }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getProctoringInfoData(courseId)
|
getProctoringInfoData(courseId, username)
|
||||||
.then(
|
.then(
|
||||||
response => {
|
response => {
|
||||||
if (response) {
|
if (response) {
|
||||||
@@ -172,7 +172,12 @@ function ProctoringInfoPanel({ courseId, intl }) {
|
|||||||
|
|
||||||
ProctoringInfoPanel.propTypes = {
|
ProctoringInfoPanel.propTypes = {
|
||||||
courseId: PropTypes.string.isRequired,
|
courseId: PropTypes.string.isRequired,
|
||||||
|
username: PropTypes.string,
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ProctoringInfoPanel.defaultProps = {
|
||||||
|
username: null,
|
||||||
|
};
|
||||||
|
|
||||||
export default injectIntl(ProctoringInfoPanel);
|
export default injectIntl(ProctoringInfoPanel);
|
||||||
|
|||||||
85
src/course-home/progress-tab/ProgressTab.test.jsx
Normal file
85
src/course-home/progress-tab/ProgressTab.test.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Factory } from 'rosie';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
|
||||||
|
import {
|
||||||
|
initializeMockApp, logUnhandledRequests, render, screen, act,
|
||||||
|
} from '../../setupTest';
|
||||||
|
import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils';
|
||||||
|
import * as thunks from '../data/thunks';
|
||||||
|
import initializeStore from '../../store';
|
||||||
|
import ProgressTab from './ProgressTab';
|
||||||
|
|
||||||
|
initializeMockApp();
|
||||||
|
jest.mock('@edx/frontend-platform/analytics');
|
||||||
|
|
||||||
|
describe('Progress Tab', () => {
|
||||||
|
let axiosMock;
|
||||||
|
|
||||||
|
const courseId = 'course-v1:edX+Test+run';
|
||||||
|
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/${courseId}`;
|
||||||
|
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||||
|
const progressUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/progress/${courseId}`;
|
||||||
|
|
||||||
|
const store = initializeStore();
|
||||||
|
const defaultMetadata = Factory.build('courseHomeMetadata', { id: courseId });
|
||||||
|
const defaultTabData = Factory.build('progressTabData');
|
||||||
|
|
||||||
|
function setTabData(attributes, options) {
|
||||||
|
const progressTabData = Factory.build('progressTabData', attributes, options);
|
||||||
|
axiosMock.onGet(progressUrl).reply(200, progressTabData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAndRender() {
|
||||||
|
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
||||||
|
await act(async () => render(<ProgressTab />, { store }));
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
|
||||||
|
// Set defaults for network requests
|
||||||
|
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
|
||||||
|
axiosMock.onGet(progressUrl).reply(200, defaultTabData);
|
||||||
|
|
||||||
|
logUnhandledRequests(axiosMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Grade Summary', () => {
|
||||||
|
it('renders Grade Summary table when assignment policies are populated', async () => {
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render Grade Summary when assignment policies are not populated', async () => {
|
||||||
|
setTabData({
|
||||||
|
grading_policy: {
|
||||||
|
assignment_policies: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.queryByText('Grade summary')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Detailed Grades', () => {
|
||||||
|
it('renders Detailed Grades table when section scores are populated', async () => {
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByRole('link', { name: 'First subsection' }));
|
||||||
|
expect(screen.getByRole('link', { name: 'Second subsection' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('render message when section scores are not populated', async () => {
|
||||||
|
setTabData({
|
||||||
|
section_scores: [],
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('You currently have no graded problem scores.')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { OverlayTrigger, Popover } from '@edx/paragon';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
function CompleteDonutSegment({ completePercentage, intl, lockedPercentage }) {
|
||||||
|
const [showCompletePopover, setShowCompletePopover] = useState(false);
|
||||||
|
|
||||||
|
const completeSegmentOffset = (3.6 * completePercentage) / 8;
|
||||||
|
let completeTooltipDegree = completePercentage < 100 ? -completeSegmentOffset : 0;
|
||||||
|
|
||||||
|
const lockedSegmentOffset = lockedPercentage - 75;
|
||||||
|
if (lockedPercentage > 0) {
|
||||||
|
completeTooltipDegree = (lockedSegmentOffset + completePercentage) * -3.6 + 90 + completeSegmentOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
className="donut-segment-group"
|
||||||
|
onBlur={() => setShowCompletePopover(false)}
|
||||||
|
onFocus={() => setShowCompletePopover(true)}
|
||||||
|
tabIndex="-1"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="donut-segment complete-stroke"
|
||||||
|
cx="21"
|
||||||
|
cy="21"
|
||||||
|
r="15.91549430918954"
|
||||||
|
strokeDasharray={`${completePercentage} ${100 - completePercentage}`}
|
||||||
|
strokeDashoffset={lockedSegmentOffset + completePercentage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tooltip */}
|
||||||
|
<OverlayTrigger
|
||||||
|
show={showCompletePopover}
|
||||||
|
placement="top"
|
||||||
|
overlay={(
|
||||||
|
<Popover aria-hidden="true">
|
||||||
|
<Popover.Content>
|
||||||
|
{intl.formatMessage(messages.completeContentTooltip)}
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Used to anchor the tooltip within the complete segment's stroke */}
|
||||||
|
<rect x="19" y="3" style={{ transform: `rotate(${completeTooltipDegree}deg)` }} />
|
||||||
|
</OverlayTrigger>
|
||||||
|
|
||||||
|
{/* Segment dividers */}
|
||||||
|
{lockedPercentage > 0 && lockedPercentage < 100 && (
|
||||||
|
<circle
|
||||||
|
className="donut-segment divider-stroke"
|
||||||
|
strokeDasharray="0.3 99.7"
|
||||||
|
strokeDashoffset={0.15 + lockedSegmentOffset}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{completePercentage < 100 && lockedPercentage < 100 && lockedPercentage + completePercentage === 100 && (
|
||||||
|
<circle
|
||||||
|
className="donut-segment divider-stroke"
|
||||||
|
strokeDasharray="0.3 99.7"
|
||||||
|
strokeDashoffset="25.15"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompleteDonutSegment.propTypes = {
|
||||||
|
completePercentage: PropTypes.number.isRequired,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
lockedPercentage: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(CompleteDonutSegment);
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
|
import CompleteDonutSegment from './CompleteDonutSegment';
|
||||||
|
import IncompleteDonutSegment from './IncompleteDonutSegment';
|
||||||
|
import LockedDonutSegment from './LockedDonutSegment';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
function CompletionDonutChart({ intl }) {
|
||||||
|
const {
|
||||||
|
courseId,
|
||||||
|
} = useSelector(state => state.courseHome);
|
||||||
|
|
||||||
|
const {
|
||||||
|
completionSummary: {
|
||||||
|
completeCount,
|
||||||
|
incompleteCount,
|
||||||
|
lockedCount,
|
||||||
|
},
|
||||||
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
|
const numTotalUnits = completeCount + incompleteCount + lockedCount;
|
||||||
|
const completePercentage = Number(((completeCount / numTotalUnits) * 100).toFixed(0));
|
||||||
|
const lockedPercentage = Number(((lockedCount / numTotalUnits) * 100).toFixed(0));
|
||||||
|
const incompletePercentage = 100 - completePercentage - lockedPercentage;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<svg role="img" width="50%" height="100%" viewBox="0 0 42 42" className="donut" style={{ maxWidth: '178px' }} aria-hidden="true">
|
||||||
|
{/* The radius (or "r" attribute) is based off of a circumference of 100 in order to simplify percentage
|
||||||
|
calculations. The subsequent stroke-dasharray values found in each segment should add up to equal 100
|
||||||
|
in order to wrap around the circle once. */}
|
||||||
|
<circle className="donut-hole" fill="#fff" cx="21" cy="21" r="15.91549430918954" />
|
||||||
|
<g className="donut-chart-text">
|
||||||
|
<text x="50%" y="50%" className="donut-chart-number">
|
||||||
|
{completePercentage}%
|
||||||
|
</text>
|
||||||
|
<text x="50%" y="50%" className="donut-chart-label">
|
||||||
|
{intl.formatMessage(messages.donutLabel)}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
<IncompleteDonutSegment incompletePercentage={incompletePercentage} />
|
||||||
|
<LockedDonutSegment lockedPercentage={lockedPercentage} />
|
||||||
|
<CompleteDonutSegment completePercentage={completePercentage} lockedPercentage={lockedPercentage} />
|
||||||
|
</svg>
|
||||||
|
<div className="sr-only">
|
||||||
|
{intl.formatMessage(messages.percentComplete, { percent: completePercentage })}
|
||||||
|
{intl.formatMessage(messages.percentIncomplete, { percent: incompletePercentage })}
|
||||||
|
{lockedPercentage > 0 && (
|
||||||
|
<>
|
||||||
|
{intl.formatMessage(messages.percentLocked, { percent: lockedPercentage })}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletionDonutChart.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(CompletionDonutChart);
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
.donut rect {
|
||||||
|
fill: transparent;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut-chart-label {
|
||||||
|
font: {
|
||||||
|
family: $font-family-sans-serif;
|
||||||
|
size: .2rem;
|
||||||
|
weight: $font-weight-normal;
|
||||||
|
}
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut-chart-number {
|
||||||
|
font: {
|
||||||
|
family: $font-family-monospace;
|
||||||
|
size: .5rem;
|
||||||
|
weight: $font-weight-bold;
|
||||||
|
}
|
||||||
|
line-height: 1rem;
|
||||||
|
text-anchor: middle;
|
||||||
|
-moz-transform: translateY(-0.6em);
|
||||||
|
-ms-transform: translateY(-0.6em);
|
||||||
|
-webkit-transform: translateY(-0.6em);
|
||||||
|
transform: translateY(-0.6em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut-chart-text {
|
||||||
|
fill: $primary-500;
|
||||||
|
-moz-transform: translateY(0.25em);
|
||||||
|
-ms-transform: translateY(0.25em);
|
||||||
|
-webkit-transform: translateY(0.25em);
|
||||||
|
transform: translateY(0.25em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut-ring, .donut-segment {
|
||||||
|
stroke-width: 6px;
|
||||||
|
fill: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut-segment-group {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: visibleStroke;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
circle {
|
||||||
|
stroke-width: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut-ring, .donut-segment, .donut-hole {
|
||||||
|
&.complete-stroke {
|
||||||
|
stroke: $info-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.divider-stroke {
|
||||||
|
stroke-width: 7px;
|
||||||
|
stroke: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.incomplete-stroke {
|
||||||
|
stroke: $light-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.locked-stroke {
|
||||||
|
stroke: $primary-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { useModel } from '../../../generic/model-store';
|
|
||||||
|
|
||||||
function CourseCompletion() {
|
import CompletionDonutChart from './CompletionDonutChart';
|
||||||
// TODO: AA-720
|
import messages from './messages';
|
||||||
const {
|
|
||||||
courseId,
|
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
|
||||||
completionSummary: {
|
|
||||||
completeCount,
|
|
||||||
incompleteCount,
|
|
||||||
lockedCount,
|
|
||||||
},
|
|
||||||
} = useModel('progress', courseId);
|
|
||||||
|
|
||||||
const total = completeCount + incompleteCount + lockedCount;
|
|
||||||
const completePercentage = ((completeCount / total) * 100).toFixed(0);
|
|
||||||
const incompletePercentage = ((incompleteCount / total) * 100).toFixed(0);
|
|
||||||
const lockedPercentage = ((lockedCount / total) * 100).toFixed(0);
|
|
||||||
|
|
||||||
|
function CourseCompletion({ intl }) {
|
||||||
return (
|
return (
|
||||||
<section className="text-dark-700 mb-4 rounded shadow-sm p-4">
|
<section className="text-dark-700 mb-4 rounded shadow-sm p-4">
|
||||||
<h2>Course completion</h2>
|
<div className="row w-100 m-0">
|
||||||
<p className="small">This represents how much course content you have completed.</p>
|
<div className="col-12 col-sm-6 col-md-7 p-0">
|
||||||
Complete: {completePercentage}%
|
<h2>{intl.formatMessage(messages.courseCompletion)}</h2>
|
||||||
Incomplete: {incompletePercentage}%
|
<p className="small">
|
||||||
Locked: {lockedPercentage}%
|
{intl.formatMessage(messages.completionBody)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
|
||||||
|
<CompletionDonutChart />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CourseCompletion;
|
CourseCompletion.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(CourseCompletion);
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { OverlayTrigger, Popover } from '@edx/paragon';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
function IncompleteDonutSegment({ incompletePercentage, intl }) {
|
||||||
|
const [showIncompletePopover, setShowIncompletePopover] = useState(false);
|
||||||
|
|
||||||
|
const incompleteSegmentOffset = (3.6 * incompletePercentage) / 16;
|
||||||
|
const incompleteTooltipDegree = incompletePercentage < 100 ? incompleteSegmentOffset : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
className="donut-segment-group"
|
||||||
|
onBlur={() => setShowIncompletePopover(false)}
|
||||||
|
onFocus={() => setShowIncompletePopover(true)}
|
||||||
|
tabIndex="-1"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="donut-ring incomplete-stroke"
|
||||||
|
cx="21"
|
||||||
|
cy="21"
|
||||||
|
r="15.91549430918954"
|
||||||
|
strokeDasharray={`${incompletePercentage} ${100 - incompletePercentage}`}
|
||||||
|
strokeDashoffset="25"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tooltip */}
|
||||||
|
<OverlayTrigger
|
||||||
|
show={showIncompletePopover}
|
||||||
|
placement="top"
|
||||||
|
overlay={(
|
||||||
|
<Popover aria-hidden="true">
|
||||||
|
<Popover.Content>
|
||||||
|
{intl.formatMessage(messages.incompleteContentTooltip)}
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Used to anchor the tooltip within the incomplete segment's stroke */}
|
||||||
|
<rect x="19" y="3" style={{ transform: `rotate(${incompleteTooltipDegree}deg)` }} />
|
||||||
|
</OverlayTrigger>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IncompleteDonutSegment.propTypes = {
|
||||||
|
incompletePercentage: PropTypes.number.isRequired,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(IncompleteDonutSegment);
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { OverlayTrigger, Popover } from '@edx/paragon';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
function LockedDonutSegment({ intl, lockedPercentage }) {
|
||||||
|
const [showLockedPopover, setShowLockedPopover] = useState(false);
|
||||||
|
|
||||||
|
if (!lockedPercentage > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconDegree = lockedPercentage > 8 ? (3.6 * lockedPercentage) / 8 : ((3.6 * lockedPercentage) / 5) * 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
className="donut-segment-group"
|
||||||
|
onBlur={() => setShowLockedPopover(false)}
|
||||||
|
onFocus={() => setShowLockedPopover(true)}
|
||||||
|
tabIndex="-1"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="donut-segment locked-stroke"
|
||||||
|
cx="21"
|
||||||
|
cy="21"
|
||||||
|
r="15.91549430918954"
|
||||||
|
strokeDasharray={`${lockedPercentage} ${100 - lockedPercentage}`}
|
||||||
|
strokeDashoffset={lockedPercentage - 75}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tooltip */}
|
||||||
|
<OverlayTrigger
|
||||||
|
show={showLockedPopover}
|
||||||
|
placement="top"
|
||||||
|
overlay={(
|
||||||
|
<Popover aria-hidden="true">
|
||||||
|
<Popover.Content>
|
||||||
|
{intl.formatMessage(messages.lockedContentTooltip)}
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
width="6"
|
||||||
|
height="21"
|
||||||
|
viewBox="0 0 21 6"
|
||||||
|
style={{
|
||||||
|
transformOrigin: 'center',
|
||||||
|
transform: `rotate(-${iconDegree}deg)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Locked icon */}
|
||||||
|
<path
|
||||||
|
d="M20 8.00002H17V6.21002C17 3.60002 15.09 1.27002 12.49 1.02002C9.51 0.740018 7 3.08002 7 6.00002V8.00002H4V22H20V8.00002ZM12 17C10.9 17 10 16.1 10 15C10 13.9 10.9 13 12 13C13.1 13 14 13.9 14 15C14 16.1 13.1 17 12 17ZM9 8.00002V6.00002C9 4.34002 10.34 3.00002 12 3.00002C13.66 3.00002 15 4.34002 15 6.00002V8.00002H9Z"
|
||||||
|
fill={lockedPercentage > 5 ? 'white' : 'transparent'}
|
||||||
|
style={{ transform: `scale(0.18) translate(5.8em, .7em) rotate(${iconDegree}deg)` }}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LockedDonutSegment.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
lockedPercentage: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(LockedDonutSegment);
|
||||||
42
src/course-home/progress-tab/course-completion/messages.js
Normal file
42
src/course-home/progress-tab/course-completion/messages.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
donutLabel: {
|
||||||
|
id: 'progress.completion.donut.label',
|
||||||
|
defaultMessage: 'completed',
|
||||||
|
},
|
||||||
|
completionBody: {
|
||||||
|
id: 'progress.completion.body',
|
||||||
|
defaultMessage: 'This represents how much of the course content you have completed. Note that some content may not yet be released.',
|
||||||
|
},
|
||||||
|
completeContentTooltip: {
|
||||||
|
id: 'progress.completion.tooltip.locked',
|
||||||
|
defaultMessage: 'Content that you have completed.',
|
||||||
|
},
|
||||||
|
courseCompletion: {
|
||||||
|
id: 'progress.completion.header',
|
||||||
|
defaultMessage: 'Course completion',
|
||||||
|
},
|
||||||
|
incompleteContentTooltip: {
|
||||||
|
id: 'progress.completion.tooltip',
|
||||||
|
defaultMessage: 'Content that you have access to and have not completed.',
|
||||||
|
},
|
||||||
|
lockedContentTooltip: {
|
||||||
|
id: 'progress.completion.tooltip.complete',
|
||||||
|
defaultMessage: 'Content that is locked and available only to those who upgrade.',
|
||||||
|
},
|
||||||
|
percentComplete: {
|
||||||
|
id: 'progress.completion.donut.percentComplete',
|
||||||
|
defaultMessage: 'You have completed {percent}% of content in this course.',
|
||||||
|
},
|
||||||
|
percentIncomplete: {
|
||||||
|
id: 'progress.completion.donut.percentIncomplete',
|
||||||
|
defaultMessage: 'You have not completed {percent}% of content in this course that you have access to.',
|
||||||
|
},
|
||||||
|
percentLocked: {
|
||||||
|
id: 'progress.completion.donut.percentLocked',
|
||||||
|
defaultMessage: '{percent}% of content in this course is locked and available only for those who upgrade.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -31,15 +31,15 @@ function DetailedGrades({ intl }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="text-dark-700 mb-4">
|
<section className="text-dark-700">
|
||||||
<h3 className="h4 mb-3">{intl.formatMessage(messages.detailedGrades)}</h3>
|
<h3 className="h4 mb-3">{intl.formatMessage(messages.detailedGrades)}</h3>
|
||||||
{hasSectionScores && (
|
{hasSectionScores && (
|
||||||
<DetailedGradesTable sectionScores={sectionScores} />
|
<DetailedGradesTable sectionScores={sectionScores} />
|
||||||
)}
|
)}
|
||||||
{!hasSectionScores && (
|
{!hasSectionScores && (
|
||||||
<p className="small">You currently have no graded problem scores.</p>
|
<p className="small">{intl.formatMessage(messages.detailedGradesEmpty)}</p>
|
||||||
)}
|
)}
|
||||||
<p className="x-small">
|
<p className="x-small m-0">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="progress.ungradedAlert"
|
id="progress.ungradedAlert"
|
||||||
defaultMessage="For progress on ungraded aspects of the course, view your {outlineLink}."
|
defaultMessage="For progress on ungraded aspects of the course, view your {outlineLink}."
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function AssignmentTypeCell({ assignmentType, footnoteMarker, footnoteId }) {
|
|||||||
AssignmentTypeCell.propTypes = {
|
AssignmentTypeCell.propTypes = {
|
||||||
assignmentType: PropTypes.string.isRequired,
|
assignmentType: PropTypes.string.isRequired,
|
||||||
footnoteId: PropTypes.string,
|
footnoteId: PropTypes.string,
|
||||||
footnoteMarker: PropTypes.string,
|
footnoteMarker: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
AssignmentTypeCell.defaultProps = {
|
AssignmentTypeCell.defaultProps = {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function GradeSummaryHeader({ intl }) {
|
|||||||
<div className="row w-100 m-0 align-items-center">
|
<div className="row w-100 m-0 align-items-center">
|
||||||
<h3 className="h4 mb-3 mr-2">{intl.formatMessage(messages.gradeSummary)}</h3>
|
<h3 className="h4 mb-3 mr-2">{intl.formatMessage(messages.gradeSummary)}</h3>
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
trigger={['hover', 'click']}
|
trigger="click"
|
||||||
placement="top"
|
placement="top"
|
||||||
overlay={(
|
overlay={(
|
||||||
<Popover>
|
<Popover>
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ const messages = defineMessages({
|
|||||||
id: 'progress.detailedGrades',
|
id: 'progress.detailedGrades',
|
||||||
defaultMessage: 'Detailed grades',
|
defaultMessage: 'Detailed grades',
|
||||||
},
|
},
|
||||||
|
detailedGradesEmpty: {
|
||||||
|
id: 'progress.detailedGrades.emptyTable',
|
||||||
|
defaultMessage: 'You currently have no graded problem scores.',
|
||||||
|
},
|
||||||
footnotesTitle: {
|
footnotesTitle: {
|
||||||
id: 'progress.footnotes.title',
|
id: 'progress.footnotes.title',
|
||||||
defaultMessage: 'Grade summary footnotes',
|
defaultMessage: 'Grade summary footnotes',
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ function CourseCelebration({ intl }) {
|
|||||||
certificateData,
|
certificateData,
|
||||||
end,
|
end,
|
||||||
linkedinAddToProfileUrl,
|
linkedinAddToProfileUrl,
|
||||||
|
marketingUrl,
|
||||||
offer,
|
offer,
|
||||||
org,
|
org,
|
||||||
relatedPrograms,
|
relatedPrograms,
|
||||||
@@ -287,7 +288,8 @@ function CourseCelebration({ intl }) {
|
|||||||
{intl.formatMessage(messages.congratulationsHeader)}
|
{intl.formatMessage(messages.congratulationsHeader)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 p-0 font-weight-normal lead text-center">
|
<div className="col-12 p-0 font-weight-normal lead text-center">
|
||||||
{intl.formatMessage(messages.shareHeader)}
|
{intl.formatMessage(messages.completedCourseHeader)}
|
||||||
|
{marketingUrl && ` ${intl.formatMessage(messages.shareMessage)}`}
|
||||||
<SocialIcons
|
<SocialIcons
|
||||||
analyticsId="edx.ui.lms.course_exit.social_share.clicked"
|
analyticsId="edx.ui.lms.course_exit.social_share.clicked"
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Sample certificate',
|
defaultMessage: 'Sample certificate',
|
||||||
description: 'Alt text used to describe an image of a certificate',
|
description: 'Alt text used to describe an image of a certificate',
|
||||||
},
|
},
|
||||||
|
completedCourseHeader: {
|
||||||
|
id: 'courseCelebration.completedCourseHeader',
|
||||||
|
defaultMessage: 'You have completed your course.',
|
||||||
|
},
|
||||||
congratulationsHeader: {
|
congratulationsHeader: {
|
||||||
id: 'courseCelebration.congratulationsHeader',
|
id: 'courseCelebration.congratulationsHeader',
|
||||||
defaultMessage: 'Congratulations!',
|
defaultMessage: 'Congratulations!',
|
||||||
@@ -127,9 +131,9 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Search our catalog',
|
defaultMessage: 'Search our catalog',
|
||||||
description: 'First part of a sentence that continues afterward',
|
description: 'First part of a sentence that continues afterward',
|
||||||
},
|
},
|
||||||
shareHeader: {
|
shareMessage: {
|
||||||
id: 'courseCelebration.shareHeader',
|
id: 'courseCelebration.shareMessage',
|
||||||
defaultMessage: 'You have completed your course. Share your success on social media or email.',
|
defaultMessage: 'Share your success on social media or email.',
|
||||||
},
|
},
|
||||||
socialMessage: {
|
socialMessage: {
|
||||||
id: 'courseExit.social.shareCompletionMessage',
|
id: 'courseExit.social.shareCompletionMessage',
|
||||||
|
|||||||
@@ -23,6 +23,20 @@ import { MMP2PLockPaywall } from '../../../experiments/mm-p2p';
|
|||||||
|
|
||||||
const LockPaywall = React.lazy(() => import('./lock-paywall'));
|
const LockPaywall = React.lazy(() => import('./lock-paywall'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature policy for iframe, allowing access to certain courseware-related media.
|
||||||
|
*
|
||||||
|
* We must use the wildcard (*) origin for each feature, as courseware content
|
||||||
|
* may be embedded in external iframes. Notably, xblock-lti-consumer is a popular
|
||||||
|
* block that iframes external course content.
|
||||||
|
|
||||||
|
* This policy was selected in conference with the edX Security Working Group.
|
||||||
|
* Changes to it should be vetted by them (security@edx.org).
|
||||||
|
*/
|
||||||
|
const IFRAME_FEATURE_POLICY = (
|
||||||
|
'microphone *; camera *; midi *; geolocation *; encrypted-media *'
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We discovered an error in Firefox where - upon iframe load - React would cease to call any
|
* We discovered an error in Firefox where - upon iframe load - React would cease to call any
|
||||||
* useEffect hooks until the user interacts with the page again. This is particularly confusing
|
* useEffect hooks until the user interacts with the page again. This is particularly confusing
|
||||||
@@ -155,7 +169,7 @@ function Unit({
|
|||||||
: (
|
: (
|
||||||
<iframe
|
<iframe
|
||||||
title={modalOptions.title}
|
title={modalOptions.title}
|
||||||
allow="microphone *; camera *; midi *; geolocation *; encrypted-media *"
|
allow={IFRAME_FEATURE_POLICY}
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
src={modalOptions.url}
|
src={modalOptions.url}
|
||||||
style={{
|
style={{
|
||||||
@@ -178,6 +192,7 @@ function Unit({
|
|||||||
id="unit-iframe"
|
id="unit-iframe"
|
||||||
title={unit.title}
|
title={unit.title}
|
||||||
src={iframeUrl}
|
src={iframeUrl}
|
||||||
|
allow={IFRAME_FEATURE_POLICY}
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
height={iframeHeight}
|
height={iframeHeight}
|
||||||
scrolling="no"
|
scrolling="no"
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faLock } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import {
|
||||||
|
Alert, Button, Icon,
|
||||||
|
} from '@edx/paragon';
|
||||||
|
import { Locked } from '@edx/paragon/icons';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import VerifiedCert from '../../../../generic/assets/edX_certificate.png';
|
import certificateLocked from '../../../../generic/assets/edX_locked_certificate.png';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
import './LockPaywall.scss';
|
||||||
|
|
||||||
function LockPaywall({
|
function LockPaywall({
|
||||||
intl,
|
intl,
|
||||||
@@ -42,33 +46,128 @@ function LockPaywall({
|
|||||||
pageName: 'in_course',
|
pageName: 'in_course',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lockIcon = (
|
||||||
|
<Icon
|
||||||
|
className="float-left"
|
||||||
|
src={Locked}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const verifiedCertLink = (
|
||||||
|
<Alert.Link
|
||||||
|
href="https://www.edx.org/verified-certificate"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages['learn.lockPaywall.list.bullet1.linktext'])}
|
||||||
|
</Alert.Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
const gradedAssignments = (
|
||||||
|
<span className="font-weight-bold">
|
||||||
|
{intl.formatMessage(messages['learn.lockPaywall.list.bullet2.boldtext'])}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
const fullAccess = (
|
||||||
|
<span className="font-weight-bold">
|
||||||
|
{intl.formatMessage(messages['learn.lockPaywall.list.bullet3.boldtext'])}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
const nonProfit = (
|
||||||
|
<span className="font-weight-bold">
|
||||||
|
{intl.formatMessage(messages['learn.lockPaywall.list.bullet4.boldtext'])}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-gray rounded d-flex justify-content-between mt-2 p-3">
|
<Alert variant="light" aria-live="off">
|
||||||
<div>
|
<div className="row">
|
||||||
<h4 className="font-weight-bold mb-2">
|
<div className="col-auto px-0">
|
||||||
<FontAwesomeIcon icon={faLock} className="text-black mr-2 ml-1" style={{ fontSize: '2rem' }} />
|
{lockIcon}
|
||||||
<span>{intl.formatMessage(messages['learn.lockPaywall.title'])}</span>
|
</div>
|
||||||
</h4>
|
|
||||||
<p className="mb-0">
|
<div className="col">
|
||||||
<span>{intl.formatMessage(messages['learn.lockPaywall.content'])}</span>
|
<h4 aria-level="3">
|
||||||
|
<span>{intl.formatMessage(messages['learn.lockPaywall.title'])}</span>
|
||||||
<a className="lock_paywall_upgrade_link" href={upgradeUrl} onClick={logClick}>
|
</h4>
|
||||||
|
|
||||||
|
<div className="mb-2 upgrade-intro">
|
||||||
|
{intl.formatMessage(messages['learn.lockPaywall.content'])}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="d-flex flex-row flex-wrap">
|
||||||
|
<div style={{ float: 'left' }} className="mr-3 mb-2">
|
||||||
|
<img
|
||||||
|
alt={intl.formatMessage(messages['learn.lockPaywall.example.alt'])}
|
||||||
|
src={certificateLocked}
|
||||||
|
className="border-0 certificate-image-banner"
|
||||||
|
style={{ height: '128px', width: '175px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mw-xs list-div">
|
||||||
|
<div className="mb-2">
|
||||||
|
{intl.formatMessage(messages['learn.lockPaywall.list.intro'])}
|
||||||
|
</div>
|
||||||
|
<ul className="fa-ul ml-4 pl-2">
|
||||||
|
<li>
|
||||||
|
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="gatedContent.paragraph.bulletOne"
|
||||||
|
defaultMessage="Earn a {verifiedCertLink} of completion to showcase on your resume"
|
||||||
|
values={{ verifiedCertLink }}
|
||||||
|
className="bullet-text"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="gatedContent.paragraph.bulletTwo"
|
||||||
|
defaultMessage="Unlock access to all course activities, including {gradedAssignments}"
|
||||||
|
values={{ gradedAssignments }}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="gatedContent.paragraph.bulletThree"
|
||||||
|
defaultMessage="{fullAccess} to course content and materials, even after the course ends"
|
||||||
|
values={{ fullAccess }}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="gatedContent.paragraph.bulletFour"
|
||||||
|
defaultMessage="Support our {nonProfit} mission at edX"
|
||||||
|
values={{ nonProfit }}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="col-md-auto p-md-0 d-md-flex align-items-md-center mr-md-3"
|
||||||
|
style={{ textAlign: 'right' }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="lock_paywall_upgrade_link"
|
||||||
|
href={upgradeUrl}
|
||||||
|
onClick={logClick}
|
||||||
|
role="link"
|
||||||
|
>
|
||||||
{intl.formatMessage(messages['learn.lockPaywall.upgrade.link'], {
|
{intl.formatMessage(messages['learn.lockPaywall.upgrade.link'], {
|
||||||
currencySymbol,
|
currencySymbol,
|
||||||
price,
|
price,
|
||||||
})}
|
})}
|
||||||
</a>
|
</Button>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</Alert>
|
||||||
<img
|
|
||||||
alt={intl.formatMessage(messages['learn.lockPaywall.example.alt'])}
|
|
||||||
src={VerifiedCert}
|
|
||||||
className="border-0"
|
|
||||||
style={{ height: '70px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
LockPaywall.propTypes = {
|
LockPaywall.propTypes = {
|
||||||
|
|||||||
12
src/courseware/course/sequence/lock-paywall/LockPaywall.scss
Normal file
12
src/courseware/course/sequence/lock-paywall/LockPaywall.scss
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
// Temporary CSS intervention until paragon list items will support icons (PAR-429)
|
||||||
|
.fa-li {
|
||||||
|
left: -31px !important;
|
||||||
|
padding-right: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 992px) and (max-width: 1100px) {
|
||||||
|
.list-div {
|
||||||
|
width: 62%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,12 +101,22 @@
|
|||||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
||||||
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
||||||
"learning.outline.sequence-due": "{description} في{assignmentDue}",
|
"learning.outline.sequence-due": "{description} في{assignmentDue}",
|
||||||
|
"progress.completion.donut.label": "completed",
|
||||||
|
"progress.completion.body": "This represents how much of the course content you have completed. Note that some content may not yet be released.",
|
||||||
|
"progress.completion.tooltip.locked": "Content that you have completed.",
|
||||||
|
"progress.completion.header": "Course completion",
|
||||||
|
"progress.completion.tooltip": "Content that you have access to and have not completed.",
|
||||||
|
"progress.completion.tooltip.complete": "Content that is locked and available only to those who upgrade.",
|
||||||
|
"progress.completion.donut.percentComplete": "You have completed {percent}% of content in this course.",
|
||||||
|
"progress.completion.donut.percentIncomplete": "You have not completed {percent}% of content in this course that you have access to.",
|
||||||
|
"progress.completion.donut.percentLocked": "{percent}% of content in this course is locked and available only for those who upgrade.",
|
||||||
"progress.ungradedAlert": "For progress on ungraded aspects of the course, view your {outlineLink}.",
|
"progress.ungradedAlert": "For progress on ungraded aspects of the course, view your {outlineLink}.",
|
||||||
"progress.footnotes.droppableAssignments": "The lowest {numDroppable, plural, one{# {assignmentType} score} other{# {assignmentType} scores}} will be dropped.",
|
"progress.footnotes.droppableAssignments": "The lowest {numDroppable, plural, one{# {assignmentType} score} other{# {assignmentType} scores}} will be dropped.",
|
||||||
"progress.assignmentType": "Assignment type",
|
"progress.assignmentType": "Assignment type",
|
||||||
"progress.footnotes.backToContent": "Back to content",
|
"progress.footnotes.backToContent": "Back to content",
|
||||||
"progress.courseOutline": "Course Outline",
|
"progress.courseOutline": "Course Outline",
|
||||||
"progress.detailedGrades": "Detailed grades",
|
"progress.detailedGrades": "Detailed grades",
|
||||||
|
"progress.detailedGrades.emptyTable": "You currently have no graded problem scores.",
|
||||||
"progress.footnotes.title": "Grade summary footnotes",
|
"progress.footnotes.title": "Grade summary footnotes",
|
||||||
"progress.gradeSummary": "Grade summary",
|
"progress.gradeSummary": "Grade summary",
|
||||||
"progress.gradeSummary.tooltip": "Your course assignment's weight is determined by your instructor. By multiplying your score by the weight for that assignment type, your weighted grade is calculated. Your weighted grade is what's used to determine if you pass the course.",
|
"progress.gradeSummary.tooltip": "Your course assignment's weight is determined by your instructor. By multiplying your score by the weight for that assignment type, your weighted grade is calculated. Your weighted grade is what's used to determine if you pass the course.",
|
||||||
@@ -233,6 +243,10 @@
|
|||||||
"learn.contentLock.content.locked": "محتوى مغلق",
|
"learn.contentLock.content.locked": "محتوى مغلق",
|
||||||
"learn.contentLock.complete.prerequisite": "يجب استيفاء المتطلبات الأساسية: '{priceqSectionName}' للوصول إلى هذا المحتوى.",
|
"learn.contentLock.complete.prerequisite": "يجب استيفاء المتطلبات الأساسية: '{priceqSectionName}' للوصول إلى هذا المحتوى.",
|
||||||
"learn.contentLock.goToSection": "انتقل إلى قسم المتطلبات الأساسية",
|
"learn.contentLock.goToSection": "انتقل إلى قسم المتطلبات الأساسية",
|
||||||
|
"gatedContent.paragraph.bulletOne": "Earn a {verifiedCertLink} of completion to showcase on your resume",
|
||||||
|
"gatedContent.paragraph.bulletTwo": "Unlock access to all course activities, including {gradedAssignments}",
|
||||||
|
"gatedContent.paragraph.bulletThree": "{fullAccess} to course content and materials, even after the course ends",
|
||||||
|
"gatedContent.paragraph.bulletFour": "Support our {nonProfit} mission at edX",
|
||||||
"learn.lockPaywall.title": "Graded assignments are locked",
|
"learn.lockPaywall.title": "Graded assignments are locked",
|
||||||
"learn.lockPaywall.content": "Upgrade to gain access to locked features like this one and get the most out of your course.",
|
"learn.lockPaywall.content": "Upgrade to gain access to locked features like this one and get the most out of your course.",
|
||||||
"learn.lockPaywall.upgrade.link": "Upgrade for {currencySymbol}{price}",
|
"learn.lockPaywall.upgrade.link": "Upgrade for {currencySymbol}{price}",
|
||||||
@@ -278,7 +292,7 @@
|
|||||||
"learn.course.tabs.navigation.overflow.menu": "المزيد...",
|
"learn.course.tabs.navigation.overflow.menu": "المزيد...",
|
||||||
"learning.offer.screenReaderPrices": "السعر الأصلي: {originalPrice}, سعر الخصم: {discountedPrice}",
|
"learning.offer.screenReaderPrices": "السعر الأصلي: {originalPrice}, سعر الخصم: {discountedPrice}",
|
||||||
"learning.upgradeButton.screenReaderInlinePrices": "السعر الأصلي: {originalPrice}",
|
"learning.upgradeButton.screenReaderInlinePrices": "السعر الأصلي: {originalPrice}",
|
||||||
"learning.upgradeButton.buttonText": "قم بالترقية ({pricing})",
|
"learning.upgradeButton.buttonText": "Upgrade for {pricing}",
|
||||||
"masquerade-widget.userName.error.generic": "حدث خطأ؛ يرجى المحاولة مرة أخرى.",
|
"masquerade-widget.userName.error.generic": "حدث خطأ؛ يرجى المحاولة مرة أخرى.",
|
||||||
"masquerade-widget.userName.input.placeholder": "اسم المستخدم أو البريد الإلكتروني",
|
"masquerade-widget.userName.input.placeholder": "اسم المستخدم أو البريد الإلكتروني",
|
||||||
"masquerade-widget.userName.input.label": "عرف كهذا المستخدم",
|
"masquerade-widget.userName.input.label": "عرف كهذا المستخدم",
|
||||||
|
|||||||
@@ -101,12 +101,22 @@
|
|||||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Apertura de la integración: {releaseDate}",
|
"learning.proctoringPanel.onboardingButtonNotOpen": "Apertura de la integración: {releaseDate}",
|
||||||
"learning.proctoringPanel.reviewRequirementsButton": "Revisar las instrucciones y los requisitos del sistema",
|
"learning.proctoringPanel.reviewRequirementsButton": "Revisar las instrucciones y los requisitos del sistema",
|
||||||
"learning.outline.sequence-due": "Fecha límite para {description}: {assignmentDue}",
|
"learning.outline.sequence-due": "Fecha límite para {description}: {assignmentDue}",
|
||||||
|
"progress.completion.donut.label": "completed",
|
||||||
|
"progress.completion.body": "This represents how much of the course content you have completed. Note that some content may not yet be released.",
|
||||||
|
"progress.completion.tooltip.locked": "Content that you have completed.",
|
||||||
|
"progress.completion.header": "Course completion",
|
||||||
|
"progress.completion.tooltip": "Content that you have access to and have not completed.",
|
||||||
|
"progress.completion.tooltip.complete": "Content that is locked and available only to those who upgrade.",
|
||||||
|
"progress.completion.donut.percentComplete": "You have completed {percent}% of content in this course.",
|
||||||
|
"progress.completion.donut.percentIncomplete": "You have not completed {percent}% of content in this course that you have access to.",
|
||||||
|
"progress.completion.donut.percentLocked": "{percent}% of content in this course is locked and available only for those who upgrade.",
|
||||||
"progress.ungradedAlert": "For progress on ungraded aspects of the course, view your {outlineLink}.",
|
"progress.ungradedAlert": "For progress on ungraded aspects of the course, view your {outlineLink}.",
|
||||||
"progress.footnotes.droppableAssignments": "The lowest {numDroppable, plural, one{# {assignmentType} score} other{# {assignmentType} scores}} will be dropped.",
|
"progress.footnotes.droppableAssignments": "The lowest {numDroppable, plural, one{# {assignmentType} score} other{# {assignmentType} scores}} will be dropped.",
|
||||||
"progress.assignmentType": "Assignment type",
|
"progress.assignmentType": "Assignment type",
|
||||||
"progress.footnotes.backToContent": "Back to content",
|
"progress.footnotes.backToContent": "Back to content",
|
||||||
"progress.courseOutline": "Course Outline",
|
"progress.courseOutline": "Course Outline",
|
||||||
"progress.detailedGrades": "Detailed grades",
|
"progress.detailedGrades": "Detailed grades",
|
||||||
|
"progress.detailedGrades.emptyTable": "You currently have no graded problem scores.",
|
||||||
"progress.footnotes.title": "Grade summary footnotes",
|
"progress.footnotes.title": "Grade summary footnotes",
|
||||||
"progress.gradeSummary": "Grade summary",
|
"progress.gradeSummary": "Grade summary",
|
||||||
"progress.gradeSummary.tooltip": "Your course assignment's weight is determined by your instructor. By multiplying your score by the weight for that assignment type, your weighted grade is calculated. Your weighted grade is what's used to determine if you pass the course.",
|
"progress.gradeSummary.tooltip": "Your course assignment's weight is determined by your instructor. By multiplying your score by the weight for that assignment type, your weighted grade is calculated. Your weighted grade is what's used to determine if you pass the course.",
|
||||||
@@ -233,15 +243,19 @@
|
|||||||
"learn.contentLock.content.locked": "Contenido Bloqueado",
|
"learn.contentLock.content.locked": "Contenido Bloqueado",
|
||||||
"learn.contentLock.complete.prerequisite": "Debe completar el prerrequisito: '{prereqSectionName}'para acceder a este contenido.",
|
"learn.contentLock.complete.prerequisite": "Debe completar el prerrequisito: '{prereqSectionName}'para acceder a este contenido.",
|
||||||
"learn.contentLock.goToSection": "Ir a la Sección de Prerrequisitos",
|
"learn.contentLock.goToSection": "Ir a la Sección de Prerrequisitos",
|
||||||
"learn.lockPaywall.title": "Graded assignments are locked",
|
"gatedContent.paragraph.bulletOne": "Obtén un {verifiedCertLink} de finalización para compartirlo en tu currículum",
|
||||||
"learn.lockPaywall.content": "Upgrade to gain access to locked features like this one and get the most out of your course.",
|
"gatedContent.paragraph.bulletTwo": "Desbloquea el acceso a todas las actividades del curso, incluidas las {gradedAssignments}",
|
||||||
"learn.lockPaywall.upgrade.link": "Upgrade for {currencySymbol}{price}",
|
"gatedContent.paragraph.bulletThree": "{fullAccess} al contenido y los materiales del curso, incluso después de que finalice",
|
||||||
|
"gatedContent.paragraph.bulletFour": "Apoya nuestra {nonProfit} en edX",
|
||||||
|
"learn.lockPaywall.title": "Las tareas calificadas están bloqueadas",
|
||||||
|
"learn.lockPaywall.content": "Cámbiate a la opción verificada para obtener acceso a funciones bloqueadas como esta y aprovechar al máximo tu curso.",
|
||||||
|
"learn.lockPaywall.upgrade.link": "Opción verificada {currencySymbol}{price}",
|
||||||
"learn.lockPaywall.example.alt": "Certificado de ejemplo",
|
"learn.lockPaywall.example.alt": "Certificado de ejemplo",
|
||||||
"learn.lockPaywall.list.intro": "When you upgrade, you:",
|
"learn.lockPaywall.list.intro": "Cuando te cambias a la opción verificada, tú:",
|
||||||
"learn.lockPaywall.list.bullet1.linktext": "verified certificate",
|
"learn.lockPaywall.list.bullet1.linktext": "certificado verificado",
|
||||||
"learn.lockPaywall.list.bullet2.boldtext": "graded assignments",
|
"learn.lockPaywall.list.bullet2.boldtext": "tareas calificadas",
|
||||||
"learn.lockPaywall.list.bullet3.boldtext": "Full access",
|
"learn.lockPaywall.list.bullet3.boldtext": "Acceso completo",
|
||||||
"learn.lockPaywall.list.bullet4.boldtext": "non-profit",
|
"learn.lockPaywall.list.bullet4.boldtext": "misión sin fines de lucro",
|
||||||
"learn.loading.content.lock": "Cargando la mensajería de contenido bloqueado...",
|
"learn.loading.content.lock": "Cargando la mensajería de contenido bloqueado...",
|
||||||
"learn.loading.learning.sequence": "Cargando la secuencia de aprendizaje...",
|
"learn.loading.learning.sequence": "Cargando la secuencia de aprendizaje...",
|
||||||
"learn.course.load.failure": "Hubo un error al cargar este curso.",
|
"learn.course.load.failure": "Hubo un error al cargar este curso.",
|
||||||
@@ -278,7 +292,7 @@
|
|||||||
"learn.course.tabs.navigation.overflow.menu": "Más...",
|
"learn.course.tabs.navigation.overflow.menu": "Más...",
|
||||||
"learning.offer.screenReaderPrices": "Precio original: {originalPrice}; precio con descuento: {discountedPrice}",
|
"learning.offer.screenReaderPrices": "Precio original: {originalPrice}; precio con descuento: {discountedPrice}",
|
||||||
"learning.upgradeButton.screenReaderInlinePrices": "Precio original: {originalPrice}",
|
"learning.upgradeButton.screenReaderInlinePrices": "Precio original: {originalPrice}",
|
||||||
"learning.upgradeButton.buttonText": "Mejora ({precio})",
|
"learning.upgradeButton.buttonText": "Upgrade for {pricing}",
|
||||||
"masquerade-widget.userName.error.generic": "Se ha producido un error. Inténtalo de nuevo.",
|
"masquerade-widget.userName.error.generic": "Se ha producido un error. Inténtalo de nuevo.",
|
||||||
"masquerade-widget.userName.input.placeholder": "Nombre de usuario o correo electrónico",
|
"masquerade-widget.userName.input.placeholder": "Nombre de usuario o correo electrónico",
|
||||||
"masquerade-widget.userName.input.label": "Hazte pasar por este usuario",
|
"masquerade-widget.userName.input.label": "Hazte pasar por este usuario",
|
||||||
|
|||||||
@@ -101,12 +101,22 @@
|
|||||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
||||||
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
||||||
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
||||||
|
"progress.completion.donut.label": "completed",
|
||||||
|
"progress.completion.body": "This represents how much of the course content you have completed. Note that some content may not yet be released.",
|
||||||
|
"progress.completion.tooltip.locked": "Content that you have completed.",
|
||||||
|
"progress.completion.header": "Course completion",
|
||||||
|
"progress.completion.tooltip": "Content that you have access to and have not completed.",
|
||||||
|
"progress.completion.tooltip.complete": "Content that is locked and available only to those who upgrade.",
|
||||||
|
"progress.completion.donut.percentComplete": "You have completed {percent}% of content in this course.",
|
||||||
|
"progress.completion.donut.percentIncomplete": "You have not completed {percent}% of content in this course that you have access to.",
|
||||||
|
"progress.completion.donut.percentLocked": "{percent}% of content in this course is locked and available only for those who upgrade.",
|
||||||
"progress.ungradedAlert": "For progress on ungraded aspects of the course, view your {outlineLink}.",
|
"progress.ungradedAlert": "For progress on ungraded aspects of the course, view your {outlineLink}.",
|
||||||
"progress.footnotes.droppableAssignments": "The lowest {numDroppable, plural, one{# {assignmentType} score} other{# {assignmentType} scores}} will be dropped.",
|
"progress.footnotes.droppableAssignments": "The lowest {numDroppable, plural, one{# {assignmentType} score} other{# {assignmentType} scores}} will be dropped.",
|
||||||
"progress.assignmentType": "Assignment type",
|
"progress.assignmentType": "Assignment type",
|
||||||
"progress.footnotes.backToContent": "Back to content",
|
"progress.footnotes.backToContent": "Back to content",
|
||||||
"progress.courseOutline": "Course Outline",
|
"progress.courseOutline": "Course Outline",
|
||||||
"progress.detailedGrades": "Detailed grades",
|
"progress.detailedGrades": "Detailed grades",
|
||||||
|
"progress.detailedGrades.emptyTable": "You currently have no graded problem scores.",
|
||||||
"progress.footnotes.title": "Grade summary footnotes",
|
"progress.footnotes.title": "Grade summary footnotes",
|
||||||
"progress.gradeSummary": "Grade summary",
|
"progress.gradeSummary": "Grade summary",
|
||||||
"progress.gradeSummary.tooltip": "Your course assignment's weight is determined by your instructor. By multiplying your score by the weight for that assignment type, your weighted grade is calculated. Your weighted grade is what's used to determine if you pass the course.",
|
"progress.gradeSummary.tooltip": "Your course assignment's weight is determined by your instructor. By multiplying your score by the weight for that assignment type, your weighted grade is calculated. Your weighted grade is what's used to determine if you pass the course.",
|
||||||
@@ -233,6 +243,10 @@
|
|||||||
"learn.contentLock.content.locked": "Content Locked",
|
"learn.contentLock.content.locked": "Content Locked",
|
||||||
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
|
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
|
||||||
"learn.contentLock.goToSection": "Go To Prerequisite Section",
|
"learn.contentLock.goToSection": "Go To Prerequisite Section",
|
||||||
|
"gatedContent.paragraph.bulletOne": "Earn a {verifiedCertLink} of completion to showcase on your resume",
|
||||||
|
"gatedContent.paragraph.bulletTwo": "Unlock access to all course activities, including {gradedAssignments}",
|
||||||
|
"gatedContent.paragraph.bulletThree": "{fullAccess} to course content and materials, even after the course ends",
|
||||||
|
"gatedContent.paragraph.bulletFour": "Support our {nonProfit} mission at edX",
|
||||||
"learn.lockPaywall.title": "Graded assignments are locked",
|
"learn.lockPaywall.title": "Graded assignments are locked",
|
||||||
"learn.lockPaywall.content": "Upgrade to gain access to locked features like this one and get the most out of your course.",
|
"learn.lockPaywall.content": "Upgrade to gain access to locked features like this one and get the most out of your course.",
|
||||||
"learn.lockPaywall.upgrade.link": "Upgrade for {currencySymbol}{price}",
|
"learn.lockPaywall.upgrade.link": "Upgrade for {currencySymbol}{price}",
|
||||||
@@ -278,7 +292,7 @@
|
|||||||
"learn.course.tabs.navigation.overflow.menu": "More...",
|
"learn.course.tabs.navigation.overflow.menu": "More...",
|
||||||
"learning.offer.screenReaderPrices": "Original price: {originalPrice}, discount price: {discountedPrice}",
|
"learning.offer.screenReaderPrices": "Original price: {originalPrice}, discount price: {discountedPrice}",
|
||||||
"learning.upgradeButton.screenReaderInlinePrices": "Original price: {originalPrice}",
|
"learning.upgradeButton.screenReaderInlinePrices": "Original price: {originalPrice}",
|
||||||
"learning.upgradeButton.buttonText": "Upgrade ({pricing})",
|
"learning.upgradeButton.buttonText": "Upgrade for {pricing}",
|
||||||
"masquerade-widget.userName.error.generic": "An error has occurred; please try again.",
|
"masquerade-widget.userName.error.generic": "An error has occurred; please try again.",
|
||||||
"masquerade-widget.userName.input.placeholder": "Username or email",
|
"masquerade-widget.userName.input.placeholder": "Username or email",
|
||||||
"masquerade-widget.userName.input.label": "Masquerade as this user",
|
"masquerade-widget.userName.input.label": "Masquerade as this user",
|
||||||
|
|||||||
@@ -101,12 +101,22 @@
|
|||||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
||||||
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
||||||
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
||||||
|
"progress.completion.donut.label": "completed",
|
||||||
|
"progress.completion.body": "This represents how much of the course content you have completed. Note that some content may not yet be released.",
|
||||||
|
"progress.completion.tooltip.locked": "Content that you have completed.",
|
||||||
|
"progress.completion.header": "Course completion",
|
||||||
|
"progress.completion.tooltip": "Content that you have access to and have not completed.",
|
||||||
|
"progress.completion.tooltip.complete": "Content that is locked and available only to those who upgrade.",
|
||||||
|
"progress.completion.donut.percentComplete": "You have completed {percent}% of content in this course.",
|
||||||
|
"progress.completion.donut.percentIncomplete": "You have not completed {percent}% of content in this course that you have access to.",
|
||||||
|
"progress.completion.donut.percentLocked": "{percent}% of content in this course is locked and available only for those who upgrade.",
|
||||||
"progress.ungradedAlert": "For progress on ungraded aspects of the course, view your {outlineLink}.",
|
"progress.ungradedAlert": "For progress on ungraded aspects of the course, view your {outlineLink}.",
|
||||||
"progress.footnotes.droppableAssignments": "The lowest {numDroppable, plural, one{# {assignmentType} score} other{# {assignmentType} scores}} will be dropped.",
|
"progress.footnotes.droppableAssignments": "The lowest {numDroppable, plural, one{# {assignmentType} score} other{# {assignmentType} scores}} will be dropped.",
|
||||||
"progress.assignmentType": "Assignment type",
|
"progress.assignmentType": "Assignment type",
|
||||||
"progress.footnotes.backToContent": "Back to content",
|
"progress.footnotes.backToContent": "Back to content",
|
||||||
"progress.courseOutline": "Course Outline",
|
"progress.courseOutline": "Course Outline",
|
||||||
"progress.detailedGrades": "Detailed grades",
|
"progress.detailedGrades": "Detailed grades",
|
||||||
|
"progress.detailedGrades.emptyTable": "You currently have no graded problem scores.",
|
||||||
"progress.footnotes.title": "Grade summary footnotes",
|
"progress.footnotes.title": "Grade summary footnotes",
|
||||||
"progress.gradeSummary": "Grade summary",
|
"progress.gradeSummary": "Grade summary",
|
||||||
"progress.gradeSummary.tooltip": "Your course assignment's weight is determined by your instructor. By multiplying your score by the weight for that assignment type, your weighted grade is calculated. Your weighted grade is what's used to determine if you pass the course.",
|
"progress.gradeSummary.tooltip": "Your course assignment's weight is determined by your instructor. By multiplying your score by the weight for that assignment type, your weighted grade is calculated. Your weighted grade is what's used to determine if you pass the course.",
|
||||||
@@ -233,6 +243,10 @@
|
|||||||
"learn.contentLock.content.locked": "Content Locked",
|
"learn.contentLock.content.locked": "Content Locked",
|
||||||
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
|
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
|
||||||
"learn.contentLock.goToSection": "Go To Prerequisite Section",
|
"learn.contentLock.goToSection": "Go To Prerequisite Section",
|
||||||
|
"gatedContent.paragraph.bulletOne": "Earn a {verifiedCertLink} of completion to showcase on your resume",
|
||||||
|
"gatedContent.paragraph.bulletTwo": "Unlock access to all course activities, including {gradedAssignments}",
|
||||||
|
"gatedContent.paragraph.bulletThree": "{fullAccess} to course content and materials, even after the course ends",
|
||||||
|
"gatedContent.paragraph.bulletFour": "Support our {nonProfit} mission at edX",
|
||||||
"learn.lockPaywall.title": "Graded assignments are locked",
|
"learn.lockPaywall.title": "Graded assignments are locked",
|
||||||
"learn.lockPaywall.content": "Upgrade to gain access to locked features like this one and get the most out of your course.",
|
"learn.lockPaywall.content": "Upgrade to gain access to locked features like this one and get the most out of your course.",
|
||||||
"learn.lockPaywall.upgrade.link": "Upgrade for {currencySymbol}{price}",
|
"learn.lockPaywall.upgrade.link": "Upgrade for {currencySymbol}{price}",
|
||||||
@@ -278,7 +292,7 @@
|
|||||||
"learn.course.tabs.navigation.overflow.menu": "More...",
|
"learn.course.tabs.navigation.overflow.menu": "More...",
|
||||||
"learning.offer.screenReaderPrices": "Original price: {originalPrice}, discount price: {discountedPrice}",
|
"learning.offer.screenReaderPrices": "Original price: {originalPrice}, discount price: {discountedPrice}",
|
||||||
"learning.upgradeButton.screenReaderInlinePrices": "Original price: {originalPrice}",
|
"learning.upgradeButton.screenReaderInlinePrices": "Original price: {originalPrice}",
|
||||||
"learning.upgradeButton.buttonText": "Upgrade ({pricing})",
|
"learning.upgradeButton.buttonText": "Upgrade for {pricing}",
|
||||||
"masquerade-widget.userName.error.generic": "An error has occurred; please try again.",
|
"masquerade-widget.userName.error.generic": "An error has occurred; please try again.",
|
||||||
"masquerade-widget.userName.input.placeholder": "Username or email",
|
"masquerade-widget.userName.input.placeholder": "Username or email",
|
||||||
"masquerade-widget.userName.input.label": "Masquerade as this user",
|
"masquerade-widget.userName.input.label": "Masquerade as this user",
|
||||||
|
|||||||
@@ -368,6 +368,7 @@
|
|||||||
@import 'course-home/dates-tab/Day.scss';
|
@import 'course-home/dates-tab/Day.scss';
|
||||||
@import 'course-home/outline-tab/widgets/UpgradeCard.scss';
|
@import 'course-home/outline-tab/widgets/UpgradeCard.scss';
|
||||||
@import 'course-home/outline-tab/widgets/ProctoringInfoPanel.scss';
|
@import 'course-home/outline-tab/widgets/ProctoringInfoPanel.scss';
|
||||||
|
@import 'course-home/progress-tab/course-completion/CompletionDonutChart.scss';
|
||||||
@import 'courseware/course/course-exit/CourseRecommendationsExp/course_recommendations.exp';
|
@import 'courseware/course/course-exit/CourseRecommendationsExp/course_recommendations.exp';
|
||||||
|
|
||||||
/** [MM-P2P] Experiment */
|
/** [MM-P2P] Experiment */
|
||||||
|
|||||||
Reference in New Issue
Block a user