AA-128: Course Home UI/UX improvements (#208)
This commit is contained in:
1
.env
1
.env
@@ -17,4 +17,5 @@ SEGMENT_KEY=null
|
||||
SITE_NAME=null
|
||||
TWITTER_URL=null
|
||||
STUDIO_BASE_URL=
|
||||
SUPPORT_URL=null
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
|
||||
@@ -17,4 +17,5 @@ SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
TWITTER_URL='https://twitter.com/edXOnline'
|
||||
STUDIO_BASE_URL='http://localhost:18010'
|
||||
SUPPORT_URL='https://support.edx.org'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
|
||||
@@ -16,4 +16,5 @@ SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
TWITTER_URL='https://twitter.com/edXOnline'
|
||||
STUDIO_BASE_URL='http://localhost:18010'
|
||||
SUPPORT_URL='https://support.edx.org'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
|
||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -1443,9 +1443,9 @@
|
||||
}
|
||||
},
|
||||
"@edx/paragon": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-10.1.1.tgz",
|
||||
"integrity": "sha512-2M8b3H2EhsHHheNblLCKkJtOJwFM6zFMLrIJYWUdhJCLNInHzNcGevrNxsiAXX8sKzpvHPkKpknrm57NJDpcIQ==",
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-12.0.0.tgz",
|
||||
"integrity": "sha512-H+lwlDzYOEltDV3zdzmgbAM4D/mFEEEeNw/fwBwt8Nk2Cbf5yqcjPINMoEh7TkclTYqqxsoIsvI/si9cVQ/rnw==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.30",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
||||
@@ -3006,9 +3006,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/invariant": {
|
||||
"version": "2.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.33.tgz",
|
||||
"integrity": "sha512-/jUNmS8d4bCKdqslfxW6dg/9Gksfzxz67IYfqApHn+HvHlMVXwYv2zpTDnS/yaK9BB0i0GlBTaYci0EFE62Hmw=="
|
||||
"version": "2.2.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.34.tgz",
|
||||
"integrity": "sha512-lYUtmJ9BqUN688fGY1U1HZoWT1/Jrmgigx2loq4ZcJpICECm/Om3V314BxdzypO0u5PORKGMM6x0OXaljV1YFg=="
|
||||
},
|
||||
"@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.3",
|
||||
@@ -3207,9 +3207,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "16.9.48",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.48.tgz",
|
||||
"integrity": "sha512-4ykBVswgYitPGMXFRxJCHkxJDU2rjfU3/zw67f8+dB7sNdVJXsrwqoYxz/stkAucymnEEbRPFmX7Ce5Mc/kJCw==",
|
||||
"version": "16.9.49",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.49.tgz",
|
||||
"integrity": "sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g==",
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -7034,9 +7034,9 @@
|
||||
}
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.0.1.tgz",
|
||||
"integrity": "sha512-1Aj1Qy3YLbdslkI75QEOfdp9TkQ3o8LRISAzxOibjBs/xWwr1WxZFOQphFkZuepHFGo+kB8e5FVJSS0faAJ4Rw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.1.0.tgz",
|
||||
"integrity": "sha512-ox7bvGXt2n+uLWtCRLybYx60IrOlWL/aCebWJk1T0d4m3y2tzf4U3ij9wBMUb6YJZpz06HCCYuyCDveE2xXmzQ==",
|
||||
"requires": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^3.0.0",
|
||||
@@ -7050,9 +7050,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"domelementtype": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
|
||||
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz",
|
||||
"integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA=="
|
||||
},
|
||||
"domexception": {
|
||||
"version": "2.0.1",
|
||||
@@ -7080,9 +7080,9 @@
|
||||
}
|
||||
},
|
||||
"domutils": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.2.0.tgz",
|
||||
"integrity": "sha512-0haAxVr1PR0SqYwCH7mxMpHZUwjih9oPPedqpR/KufsnxPyZ9dyVw1R5093qnJF3WXSbjBkdzRWLw/knJV/fAg==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.3.0.tgz",
|
||||
"integrity": "sha512-xWC75PM3QF6MjE5e58OzwTX0B/rPQnlqH0YyXB/c056RtVJA+eu60da2I/bdnEHzEYC00g8QaZUlAbqOZVbOsw==",
|
||||
"requires": {
|
||||
"dom-serializer": "^1.0.1",
|
||||
"domelementtype": "^2.0.1",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@edx/frontend-component-header": "2.0.5",
|
||||
"@edx/frontend-enterprise": "4.2.2",
|
||||
"@edx/frontend-platform": "1.5.2",
|
||||
"@edx/paragon": "10.1.1",
|
||||
"@edx/paragon": "12.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.30",
|
||||
"@fortawesome/free-brands-svg-icons": "5.13.1",
|
||||
"@fortawesome/free-regular-svg-icons": "5.13.1",
|
||||
|
||||
@@ -78,13 +78,13 @@ function Header({
|
||||
|
||||
return (
|
||||
<header className="course-header">
|
||||
<div className="container-fluid py-2 d-flex align-items-center ">
|
||||
<div className="container-fluid py-2 d-flex align-items-center">
|
||||
{headerLogo}
|
||||
<div className="flex-grow-1 course-title-lockup" style={{ lineHeight: 1 }}>
|
||||
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
|
||||
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
|
||||
</div>
|
||||
|
||||
<a className="text-gray-700 mr-3" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
||||
<Dropdown className="user-dropdown">
|
||||
<Dropdown.Toggle variant="light">
|
||||
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
||||
|
||||
@@ -11,6 +11,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Dashboard',
|
||||
description: 'The text for the user menu Dashboard navigation link.',
|
||||
},
|
||||
help: {
|
||||
id: 'header.help.label',
|
||||
defaultMessage: 'Help',
|
||||
description: 'The text for the link to the Help Center',
|
||||
},
|
||||
profile: {
|
||||
id: 'header.menu.profile.label',
|
||||
defaultMessage: 'Profile',
|
||||
|
||||
@@ -7,7 +7,7 @@ Object {
|
||||
"courseStatus": "loading",
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": Object {
|
||||
"courseId": null,
|
||||
@@ -26,7 +26,7 @@ Object {
|
||||
"courseStatus": "loaded",
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": Object {
|
||||
"courseId": null,
|
||||
@@ -112,7 +112,7 @@ Object {
|
||||
"courseStatus": "loaded",
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": Object {
|
||||
"courseId": null,
|
||||
|
||||
@@ -12,7 +12,7 @@ const slice = createSlice({
|
||||
courseId: null,
|
||||
toastBodyText: null,
|
||||
toastBodyLink: null,
|
||||
toastHeader: null,
|
||||
toastHeader: '',
|
||||
},
|
||||
reducers: {
|
||||
fetchTabRequest: (state, { payload }) => {
|
||||
|
||||
@@ -15,8 +15,8 @@ export default function DateSummary({
|
||||
return (
|
||||
<section className="container p-0 mb-3">
|
||||
<div className="row">
|
||||
<FontAwesomeIcon icon={faCalendarAlt} className="ml-3 mt-1 mr-1" style={{ width: '20px' }} />
|
||||
<div className="ml-2 font-weight-bold">
|
||||
<FontAwesomeIcon icon={faCalendarAlt} className="ml-3 mt-1 mr-1" fixedWidth />
|
||||
<div className="ml-1 font-weight-bold">
|
||||
<FormattedDate
|
||||
value={dateBlock.date}
|
||||
day="numeric"
|
||||
@@ -27,7 +27,7 @@ export default function DateSummary({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row ml-4 px-2">
|
||||
<div className="row ml-4 pl-1 pr-2">
|
||||
<div className="date-summary-text">
|
||||
{linkedTitle
|
||||
&& <div className="font-weight-bold mt-2"><a href={dateBlock.link}>{dateBlock.title}</a></div>}
|
||||
|
||||
@@ -2,13 +2,14 @@ import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Button, Toast } from '@edx/paragon';
|
||||
import { AlertList } from '../../generic/user-messages';
|
||||
|
||||
import CourseDates from './widgets/CourseDates';
|
||||
import CourseGoalCard from './widgets/CourseGoalCard';
|
||||
import CourseHandouts from './widgets/CourseHandouts';
|
||||
import CourseTools from './widgets/CourseTools';
|
||||
import LearningToast from '../../toast/LearningToast';
|
||||
import genericMessages from '../../generic/messages';
|
||||
import messages from './messages';
|
||||
import Section from './Section';
|
||||
import UpdateGoalSelector from './widgets/UpdateGoalSelector';
|
||||
@@ -55,7 +56,8 @@ function OutlineTab({ intl }) {
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
const [courseGoalToDisplay, setCourseGoalToDisplay] = useState(selectedGoal);
|
||||
const [goalToastHeader, setGoalToastHeader] = useState(null);
|
||||
const [goalToastHeader, setGoalToastHeader] = useState('');
|
||||
const [expandAll, setExpandAll] = useState(false);
|
||||
|
||||
// Above the tab alerts (appearing in the order listed here)
|
||||
const logistrationAlert = useLogistrationAlert();
|
||||
@@ -81,17 +83,23 @@ function OutlineTab({ intl }) {
|
||||
...logistrationAlert,
|
||||
}}
|
||||
/>
|
||||
<LearningToast
|
||||
header={goalToastHeader}
|
||||
onClose={() => setGoalToastHeader(null)}
|
||||
<Toast
|
||||
closeLabel={intl.formatMessage(genericMessages.close)}
|
||||
onClose={() => setGoalToastHeader('')}
|
||||
show={!!(goalToastHeader)}
|
||||
/>
|
||||
<div className="d-flex justify-content-between mb-3">
|
||||
<div role="heading" aria-level="1" className="h4">{title}</div>
|
||||
>
|
||||
{goalToastHeader}
|
||||
</Toast>
|
||||
<div className="row w-100 m-0 mb-3 justify-content-between">
|
||||
<div className="col-12 col-sm-auto p-0">
|
||||
<div role="heading" aria-level="1" className="h4">{title}</div>
|
||||
</div>
|
||||
{resumeCourseUrl && (
|
||||
<a className="btn btn-primary" href={resumeCourseUrl}>
|
||||
{hasVisitedCourse ? intl.formatMessage(messages.resume) : intl.formatMessage(messages.start)}
|
||||
</a>
|
||||
<div className="col-12 col-sm-auto p-0">
|
||||
<a className="btn btn-primary btn-block" href={resumeCourseUrl}>
|
||||
{hasVisitedCourse ? intl.formatMessage(messages.resume) : intl.formatMessage(messages.start)}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="row">
|
||||
@@ -117,10 +125,18 @@ function OutlineTab({ intl }) {
|
||||
...offerAlert,
|
||||
}}
|
||||
/>
|
||||
<div className="row w-100 m-0 mb-3 justify-content-end">
|
||||
<div className="col-12 col-sm-auto p-0">
|
||||
<Button variant="outline-primary" block onClick={() => { setExpandAll(!expandAll); }}>
|
||||
{expandAll ? intl.formatMessage(messages.collapseAll) : intl.formatMessage(messages.expandAll)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{sectionIds.map((sectionId) => (
|
||||
<Section
|
||||
key={sectionId}
|
||||
courseId={courseId}
|
||||
expand={expandAll}
|
||||
section={sections[sectionId]}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible, IconButton } from '@edx/paragon';
|
||||
import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import SequenceLink from './SequenceLink';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import genericMessages from '../../generic/messages';
|
||||
import messages from './messages';
|
||||
|
||||
export default function Section({ courseId, section }) {
|
||||
function Section({
|
||||
courseId,
|
||||
expand,
|
||||
intl,
|
||||
section,
|
||||
}) {
|
||||
const {
|
||||
complete,
|
||||
sequenceIds,
|
||||
@@ -18,15 +27,60 @@ export default function Section({ courseId, section }) {
|
||||
},
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
const [open, setOpen] = useState(expand);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(expand);
|
||||
}, [expand]);
|
||||
|
||||
const sectionTitle = (
|
||||
<div>
|
||||
{complete && <FontAwesomeIcon icon={faCheckCircle} className="float-left text-success mt-1" />}
|
||||
<div className="ml-4 font-weight-bold">{title}</div>
|
||||
{complete ? (
|
||||
<FontAwesomeIcon
|
||||
icon={fasCheckCircle}
|
||||
className="float-left mt-1 text-success"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.completedSection)}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={farCheckCircle}
|
||||
className="float-left mt-1 text-gray-200"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.incompleteSection)}
|
||||
/>
|
||||
)}
|
||||
<div className="ml-3 pl-3 font-weight-bold">
|
||||
{title}
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Collapsible className="mb-2" styling="card-lg" title={sectionTitle} defaultOpen>
|
||||
<Collapsible
|
||||
className="mb-2"
|
||||
styling="card-lg"
|
||||
title={sectionTitle}
|
||||
open={open}
|
||||
onToggle={() => { setOpen(!open); }}
|
||||
iconWhenClosed={(
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.openSection)}
|
||||
icon={faPlus}
|
||||
onClick={() => { setOpen(true); }}
|
||||
/>
|
||||
)}
|
||||
iconWhenOpen={(
|
||||
<IconButton
|
||||
alt={intl.formatMessage(genericMessages.close)}
|
||||
icon={faMinus}
|
||||
onClick={() => { setOpen(false); }}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{sequenceIds.map((sequenceId, index) => (
|
||||
<SequenceLink
|
||||
key={sequenceId}
|
||||
@@ -42,5 +96,9 @@ export default function Section({ courseId, section }) {
|
||||
|
||||
Section.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
expand: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
section: PropTypes.shape().isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(Section);
|
||||
|
||||
@@ -2,21 +2,22 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FormattedMessage, FormattedTime } from '@edx/frontend-platform/i18n';
|
||||
import { faClock, faEdit } from '@fortawesome/free-regular-svg-icons';
|
||||
import {
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faExclamationTriangle,
|
||||
faSpinner,
|
||||
faTimesCircle,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
FormattedMessage,
|
||||
FormattedTime,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { faCheckCircle as fasCheckCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
export default function SequenceLink({
|
||||
function SequenceLink({
|
||||
id,
|
||||
intl,
|
||||
courseId,
|
||||
first,
|
||||
sequence,
|
||||
@@ -25,7 +26,6 @@ export default function SequenceLink({
|
||||
complete,
|
||||
description,
|
||||
due,
|
||||
icon,
|
||||
showLink,
|
||||
title,
|
||||
} = sequence;
|
||||
@@ -37,74 +37,71 @@ export default function SequenceLink({
|
||||
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
let text = title;
|
||||
|
||||
let faIcon;
|
||||
switch (icon) {
|
||||
// list of possible ones here: https://github.com/edx/edx-proctoring/blob/master/edx_proctoring/api.py
|
||||
case 'fa-check': faIcon = faCheck; break;
|
||||
case 'fa-clock-o': faIcon = faClock; break;
|
||||
case 'fa-exclamation-triangle': faIcon = faExclamationTriangle; break;
|
||||
case 'fa-pencil-square-o': faIcon = faEdit; break;
|
||||
case 'fa-spinner fa-spin': faIcon = faSpinner; break;
|
||||
case 'fa-times-circle': faIcon = faTimesCircle; break;
|
||||
default: faIcon = null; break;
|
||||
}
|
||||
if (faIcon) {
|
||||
text = <><FontAwesomeIcon icon={faIcon} /> {text}</>;
|
||||
}
|
||||
|
||||
if (due) {
|
||||
text = (
|
||||
<>
|
||||
{text}<br />
|
||||
<small className="text-body">
|
||||
<FormattedMessage
|
||||
id="learning.outline.sequence-due"
|
||||
defaultMessage="{description} due {assignmentDue}"
|
||||
description="Used below an assignment title"
|
||||
values={{
|
||||
assignmentDue: (
|
||||
<FormattedTime
|
||||
key={`${id}-due`}
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
hour12={false}
|
||||
timeZoneName="short"
|
||||
value={due}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
description: description || '',
|
||||
}}
|
||||
/>
|
||||
</small>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
text = <div className="ml-4">{text}</div>;
|
||||
|
||||
if (complete) {
|
||||
text = <><FontAwesomeIcon icon={faCheckCircle} className="float-left text-success mt-1" />{text}</>;
|
||||
}
|
||||
|
||||
// Do link last so we include everything above in the link
|
||||
if (showLink) {
|
||||
text = <Link to={`/course/${courseId}/${id}`}><div>{text}</div></Link>;
|
||||
}
|
||||
const displayTitle = showLink ? <Link to={`/course/${courseId}/${id}`}>{title}</Link> : title;
|
||||
|
||||
return (
|
||||
<div className={classNames('', { 'mt-2 pt-2 border-top border-light': !first })}>
|
||||
{text}
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-auto p-0">
|
||||
{complete ? (
|
||||
<FontAwesomeIcon
|
||||
icon={fasCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left text-success mt-1"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.completedAssignment)}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={farCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left text-gray-200 mt-1"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.incompleteAssignment)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-10 p-0 ml-2 pl-1 text-break">{displayTitle}</div>
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
|
||||
</span>
|
||||
</div>
|
||||
{due && (
|
||||
<div className="row w-100 m-0 ml-3 pl-3">
|
||||
<small className="text-body">
|
||||
<FormattedMessage
|
||||
id="learning.outline.sequence-due"
|
||||
defaultMessage="{description} due {assignmentDue}"
|
||||
description="Used below an assignment title"
|
||||
values={{
|
||||
assignmentDue: (
|
||||
<FormattedTime
|
||||
key={`${id}-due`}
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
hour12={false}
|
||||
timeZoneName="short"
|
||||
value={due}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
description: description || '',
|
||||
}}
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SequenceLink.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
first: PropTypes.bool.isRequired,
|
||||
sequence: PropTypes.shape().isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SequenceLink);
|
||||
|
||||
@@ -5,6 +5,21 @@ const messages = defineMessages({
|
||||
id: 'learning.outline.dates.all',
|
||||
defaultMessage: 'View all course dates',
|
||||
},
|
||||
collapseAll: {
|
||||
id: 'learning.outline.collapseAll',
|
||||
defaultMessage: 'Collapse All',
|
||||
description: 'Label for button to close all of the collapsible sections',
|
||||
},
|
||||
completedAssignment: {
|
||||
id: 'learning.outline.completedAssignment',
|
||||
defaultMessage: 'Completed',
|
||||
description: 'Text used to describe the green checkmark icon in front of an assignment title',
|
||||
},
|
||||
completedSection: {
|
||||
id: 'learning.outline.completedSection',
|
||||
defaultMessage: 'Completed section',
|
||||
description: 'Text used to describe the green checkmark icon in front of a section title',
|
||||
},
|
||||
dates: {
|
||||
id: 'learning.outline.dates',
|
||||
defaultMessage: 'Upcoming Dates',
|
||||
@@ -14,6 +29,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Edit goal',
|
||||
description: 'Edit course goal button',
|
||||
},
|
||||
expandAll: {
|
||||
id: 'learning.outline.expandAll',
|
||||
defaultMessage: 'Expand All',
|
||||
description: 'Label for button to open all of the collapsible sections',
|
||||
},
|
||||
goal: {
|
||||
id: 'learning.outline.goal',
|
||||
defaultMessage: 'Goal',
|
||||
@@ -32,6 +52,21 @@ const messages = defineMessages({
|
||||
id: 'learning.outline.handouts',
|
||||
defaultMessage: 'Course Handouts',
|
||||
},
|
||||
incompleteAssignment: {
|
||||
id: 'learning.outline.incompleteAssignment',
|
||||
defaultMessage: 'Incomplete',
|
||||
description: 'Text used to describe the gray checkmark icon in front of an assignment title',
|
||||
},
|
||||
incompleteSection: {
|
||||
id: 'learning.outline.incompleteSection',
|
||||
defaultMessage: 'Incomplete section',
|
||||
description: 'Text used to describe the gray checkmark icon in front of a section title',
|
||||
},
|
||||
openSection: {
|
||||
id: 'learning.outline.altText.openSection',
|
||||
defaultMessage: 'Open',
|
||||
description: 'A button to open the given section of the course outline',
|
||||
},
|
||||
resume: {
|
||||
id: 'learning.outline.resume',
|
||||
defaultMessage: 'Resume Course',
|
||||
|
||||
@@ -9,20 +9,24 @@ import { useModel } from '../../../generic/model-store';
|
||||
|
||||
function CourseDates({ courseId, intl }) {
|
||||
const {
|
||||
datesWidget,
|
||||
datesWidget: {
|
||||
courseDateBlocks,
|
||||
datesTabLink,
|
||||
userTimezone,
|
||||
},
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
return (
|
||||
<section className="mb-3">
|
||||
<h2 className="h6">{intl.formatMessage(messages.dates)}</h2>
|
||||
{datesWidget.courseDateBlocks.map((courseDateBlock) => (
|
||||
{courseDateBlocks.map((courseDateBlock) => (
|
||||
<DateSummary
|
||||
key={courseDateBlock.title + courseDateBlock.date}
|
||||
dateBlock={courseDateBlock}
|
||||
userTimezone={datesWidget.userTimezone}
|
||||
userTimezone={userTimezone}
|
||||
/>
|
||||
))}
|
||||
<a className="font-weight-bold ml-4 pl-2" href={datesWidget.datesTabLink}>
|
||||
<a className="font-weight-bold ml-4 pl-1" href={datesTabLink}>
|
||||
{intl.formatMessage(messages.allDates)}
|
||||
</a>
|
||||
</section>
|
||||
|
||||
@@ -52,7 +52,7 @@ function CourseTools({ courseId, intl }) {
|
||||
{courseTools.map((courseTool) => (
|
||||
<div key={courseTool.analyticsId}>
|
||||
<a href={courseTool.url} onClick={() => logClick(courseTool.analyticsId)}>
|
||||
<FontAwesomeIcon icon={renderIcon(courseTool.analyticsId)} className="mr-2" style={{ width: '20px' }} />
|
||||
<FontAwesomeIcon icon={renderIcon(courseTool.analyticsId)} className="mr-2" fixedWidth />
|
||||
{courseTool.title}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Card, Input } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
|
||||
import messages from '../messages';
|
||||
import { saveCourseGoal } from '../../data';
|
||||
@@ -17,18 +15,14 @@ function UpdateGoalSelector({
|
||||
setGoalToDisplay,
|
||||
setGoalToastHeader,
|
||||
}) {
|
||||
const [editingGoal, setEditingGoal] = useState(false);
|
||||
|
||||
function selectGoalHandler(event) {
|
||||
const key = event.currentTarget.value;
|
||||
const { options } = event.currentTarget;
|
||||
const { text } = options[options.selectedIndex];
|
||||
const key = event.currentTarget.id;
|
||||
const text = event.currentTarget.innerText;
|
||||
const newGoal = {
|
||||
key,
|
||||
text,
|
||||
};
|
||||
|
||||
setEditingGoal(false);
|
||||
setGoalToDisplay(newGoal);
|
||||
saveCourseGoal(courseId, key).then((response) => {
|
||||
const { data } = response;
|
||||
@@ -45,44 +39,28 @@ function UpdateGoalSelector({
|
||||
<section className="mb-3">
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-12 p-0">
|
||||
<label className="h6" htmlFor="edit-goal-selector">
|
||||
<label className="h6 m-0" htmlFor="edit-goal-selector">
|
||||
{intl.formatMessage(messages.goal)}
|
||||
</label>
|
||||
</div>
|
||||
<div className="col-12 p-0">
|
||||
<Card>
|
||||
<Card.Body className="px-3 py-2">
|
||||
<div className="row w-100 m-0 justify-content-between align-items-center">
|
||||
<div className="col-10 p-0">
|
||||
{!editingGoal && (
|
||||
<p className="m-0">{selectedGoal.text}</p>
|
||||
)}
|
||||
{editingGoal && (
|
||||
<Input
|
||||
id="edit-goal-selector"
|
||||
type="select"
|
||||
defaultValue={selectedGoal.key}
|
||||
onBlur={() => { setEditingGoal(false); }}
|
||||
onChange={(event) => { selectGoalHandler(event); }}
|
||||
options={goalOptions.map(([goalKey, goalText]) => (
|
||||
{ value: goalKey, label: goalText }
|
||||
))}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
aria-label={intl.formatMessage(messages.editGoal)}
|
||||
className="p-1"
|
||||
size="sm"
|
||||
variant="light"
|
||||
onClick={() => { setEditingGoal(true); }}
|
||||
<Dropdown className="py-2">
|
||||
<Dropdown.Toggle variant="outline-primary" block id="edit-goal-selector">
|
||||
{selectedGoal.text}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{goalOptions.map(([goalKey, goalText]) => (
|
||||
<Dropdown.Item
|
||||
id={goalKey}
|
||||
key={goalKey}
|
||||
onClick={(event) => { selectGoalHandler(event); }}
|
||||
role="button"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilAlt} />
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
{goalText}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, TransitionReplace } from '@edx/paragon';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import LmsHtmlFragment from '../LmsHtmlFragment';
|
||||
@@ -34,27 +35,36 @@ function WelcomeMessage({ courseId, intl }) {
|
||||
setDisplay(false);
|
||||
dispatch(dismissWelcomeMessage(courseId));
|
||||
}}
|
||||
>
|
||||
<div className="my-3">
|
||||
<LmsHtmlFragment
|
||||
html={showShortMessage ? shortWelcomeMessageHtml : welcomeMessageHtml}
|
||||
title={intl.formatMessage(messages.welcomeMessage)}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
shortWelcomeMessageHtml && (
|
||||
<div className="d-flex justify-content-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn rounded align-self-center border border-primary bg-white font-weight-bold mb-3"
|
||||
footer={shortWelcomeMessageHtml && (
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-12 col-sm-auto p-0">
|
||||
<Button
|
||||
block
|
||||
onClick={() => setShowShortMessage(!showShortMessage)}
|
||||
variant="outline-primary"
|
||||
>
|
||||
{showShortMessage ? intl.formatMessage(messages.welcomeMessageShowMoreButton)
|
||||
: intl.formatMessage(messages.welcomeMessageShowLessButton)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<TransitionReplace className="mb-3" enterDuration={200} exitDuration={200}>
|
||||
{showShortMessage ? (
|
||||
<LmsHtmlFragment
|
||||
key="short-html"
|
||||
html={shortWelcomeMessageHtml}
|
||||
title={intl.formatMessage(messages.welcomeMessage)}
|
||||
/>
|
||||
) : (
|
||||
<LmsHtmlFragment
|
||||
key="full-html"
|
||||
html={welcomeMessageHtml}
|
||||
title={intl.formatMessage(messages.welcomeMessage)}
|
||||
/>
|
||||
)}
|
||||
</TransitionReplace>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
|
||||
11
src/generic/messages.js
Normal file
11
src/generic/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: {
|
||||
id: 'general.altText.close',
|
||||
defaultMessage: 'Close',
|
||||
description: 'Text used as an aria-label to describe closing or dismissing a component',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -5,10 +5,12 @@ import {
|
||||
faExclamationTriangle, faInfoCircle, faCheckCircle, faMinusCircle, faTimes,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { IconButton } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { ALERT_TYPES } from './UserMessagesProvider';
|
||||
import './Alert.scss';
|
||||
import messages from '../messages';
|
||||
|
||||
function getAlertClass(type) {
|
||||
if (type === ALERT_TYPES.ERROR) {
|
||||
@@ -40,21 +42,33 @@ function getAlertIcon(type) {
|
||||
}
|
||||
|
||||
function Alert({
|
||||
type, dismissible, children, onDismiss,
|
||||
type, dismissible, children, footer, intl, onDismiss,
|
||||
}) {
|
||||
return (
|
||||
<div className={classNames('alert', { 'alert-dismissible': dismissible }, getAlertClass(type))}>
|
||||
<div className="d-flex align-items-start">
|
||||
<div className={classNames('alert', { 'alert-dismissible': dismissible }, getAlertClass(type))} style={{ padding: '20px' }}>
|
||||
<div className="row w-100 m-0">
|
||||
{type !== ALERT_TYPES.WELCOME && (
|
||||
<div className="mr-2">
|
||||
<div className="col-auto p-0 mr-2">
|
||||
<FontAwesomeIcon icon={getAlertIcon(type)} />
|
||||
</div>
|
||||
)}
|
||||
<div role="alert" className="flex-grow-1">
|
||||
{children}
|
||||
<div className="col mr-4 p-0 align-items-start">
|
||||
<div role="alert" className="flex-grow-1">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{dismissible && (
|
||||
<div className="col-auto p-0">
|
||||
<IconButton
|
||||
icon={faTimes}
|
||||
className="close"
|
||||
onClick={onDismiss}
|
||||
alt={intl.formatMessage(messages.close)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{dismissible && <Button className="close" onClick={onDismiss}><FontAwesomeIcon size="sm" icon={faTimes} /></Button>}
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -69,13 +83,16 @@ Alert.propTypes = {
|
||||
]).isRequired,
|
||||
dismissible: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
footer: PropTypes.node,
|
||||
intl: intlShape.isRequired,
|
||||
onDismiss: PropTypes.func,
|
||||
};
|
||||
|
||||
Alert.defaultProps = {
|
||||
dismissible: false,
|
||||
children: undefined,
|
||||
footer: null,
|
||||
onDismiss: null,
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
export default injectIntl(Alert);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
.alert-welcome {
|
||||
border: #b9babe solid 1px !important;
|
||||
border-left: #000000 solid 3px !important;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ initialize({
|
||||
mergeConfig({
|
||||
INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null,
|
||||
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null,
|
||||
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
||||
TWITTER_URL: process.env.TWITTER_URL || null,
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
|
||||
}, 'LearnerAppConfig');
|
||||
|
||||
@@ -61,6 +61,7 @@ $primary: #1176B2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-bottom: .1rem;
|
||||
}
|
||||
}
|
||||
.user-dropdown {
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function InstructorToolbar(props) {
|
||||
const [masqueradeErrorMessage, showMasqueradeError] = useState(null);
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-primary text-light">
|
||||
<div className="bg-primary text-white">
|
||||
<div className="container-fluid py-3 d-md-flex justify-content-end align-items-start">
|
||||
<div className="align-items-center flex-grow-1 d-md-flex mx-1 my-1">
|
||||
<MasqueradeWidget courseId={courseId} onError={showMasqueradeError} />
|
||||
|
||||
@@ -47,6 +47,7 @@ class MasqueradeUserNameInput extends Component {
|
||||
} = this.props;
|
||||
return (
|
||||
<Input
|
||||
aria-labelledby="masquerade-search-label"
|
||||
label={intl.formatMessage(messages.userNameLabel)}
|
||||
onKeyPress={(event) => this.onKeyPress(event)}
|
||||
type="text"
|
||||
|
||||
@@ -132,7 +132,7 @@ class MasqueradeWidget extends Component {
|
||||
</div>
|
||||
{shouldShowUserNameInput && (
|
||||
<div className="row mt-2">
|
||||
<span className="col-auto col-form-label pl-3">{`${specificLearnerInputText}:`}</span>
|
||||
<span className="col-auto col-form-label pl-3" id="masquerade-search-label">{`${specificLearnerInputText}:`}</span>
|
||||
<MasqueradeUserNameInput
|
||||
id="masquerade-search"
|
||||
className="col-4 form-control"
|
||||
|
||||
@@ -3,12 +3,13 @@ import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { Toast } from '@edx/paragon';
|
||||
import { Header } from '../course-header';
|
||||
import PageLoading from '../generic/PageLoading';
|
||||
|
||||
import genericMessages from '../generic/messages';
|
||||
import messages from './messages';
|
||||
import LoadedTabPage from './LoadedTabPage';
|
||||
import LearningToast from '../toast/LearningToast';
|
||||
import { setCallToActionToast } from '../course-home/data/slice';
|
||||
|
||||
function TabPage({
|
||||
@@ -37,13 +38,17 @@ function TabPage({
|
||||
if (courseStatus === 'loaded') {
|
||||
return (
|
||||
<>
|
||||
<LearningToast
|
||||
bodyLink={toastBodyLink}
|
||||
bodyText={toastBodyText}
|
||||
header={toastHeader}
|
||||
onClose={() => dispatch(setCallToActionToast({ header: null, link: null, link_text: null }))}
|
||||
<Toast
|
||||
action={toastBodyText ? {
|
||||
label: toastBodyText,
|
||||
href: toastBodyLink,
|
||||
} : null}
|
||||
closeLabel={intl.formatMessage(genericMessages.close)}
|
||||
onClose={() => dispatch(setCallToActionToast({ header: '', link: null, link_text: null }))}
|
||||
show={!!(toastHeader)}
|
||||
/>
|
||||
>
|
||||
{toastHeader}
|
||||
</Toast>
|
||||
<LoadedTabPage {...passthroughProps} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Toast } from '@edx/paragon';
|
||||
|
||||
import './LearningToast.scss';
|
||||
|
||||
export default function LearningToast({
|
||||
bodyLink,
|
||||
bodyText,
|
||||
header,
|
||||
onClose,
|
||||
show,
|
||||
}) {
|
||||
return (
|
||||
<Toast
|
||||
onClose={onClose}
|
||||
show={show}
|
||||
delay={5000}
|
||||
autohide
|
||||
className="bg-gray-700 learning-toast"
|
||||
style={{
|
||||
boxShadow: 'none',
|
||||
position: 'fixed',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<Toast.Header className="bg-gray-700 border-bottom-0 p-0 text-light justify-content-between align-items-center">
|
||||
<p className="small m-0 mr-4">{header}</p>
|
||||
</Toast.Header>
|
||||
{bodyLink && bodyText && (
|
||||
<Toast.Body className="bg-gray-700 text-light p-0 pt-3">
|
||||
<a className="btn btn-sm btn-outline-light" href={bodyLink}>{bodyText}</a>
|
||||
</Toast.Body>
|
||||
)}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
LearningToast.propTypes = {
|
||||
bodyLink: PropTypes.string,
|
||||
bodyText: PropTypes.string,
|
||||
header: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
LearningToast.defaultProps = {
|
||||
bodyLink: null,
|
||||
bodyText: null,
|
||||
header: null,
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
.learning-toast {
|
||||
bottom: 20px;
|
||||
radius: 3px;
|
||||
left: 20px;
|
||||
max-width: 400px;
|
||||
padding: 11px 20px;
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-header {
|
||||
button {
|
||||
align-self: flex-start;
|
||||
color: #FFFFFF;
|
||||
&:hover {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toast-body {
|
||||
a:hover {
|
||||
color: #2D323E;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user