diff --git a/package-lock.json b/package-lock.json index b52de281..e9e6558b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3802,6 +3802,19 @@ "react-transition-group": "^4.0.0", "tabbable": "^4.0.0", "uncontrollable": "7.2.1" + }, + "dependencies": { + "react-responsive": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.2.0.tgz", + "integrity": "sha512-iagCqVrw4QSjhxKp3I/YK6+ODkWY6G+YPElvdYKiUUbywwh9Ds0M7r26Fj2/7dWFFbOpcGnJE6uE7aMck8j5Qg==", + "requires": { + "hyphenate-style-name": "^1.0.0", + "matchmediaquery": "^0.3.0", + "prop-types": "^15.6.1", + "shallow-equal": "^1.1.0" + } + } } }, "@formatjs/ecma402-abstract": { diff --git a/package.json b/package.json index 83392d24..b031836e 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "react-dom": "17.0.2", "react-helmet": "6.1.0", "react-redux": "7.2.5", + "react-responsive": "8.2.0", "react-router": "5.2.1", "react-router-dom": "5.3.0", "react-share": "4.4.0", diff --git a/src/alerts/logistration-alert/AccountActivationAlert.jsx b/src/alerts/logistration-alert/AccountActivationAlert.jsx index bbb9f565..dcdd06d7 100644 --- a/src/alerts/logistration-alert/AccountActivationAlert.jsx +++ b/src/alerts/logistration-alert/AccountActivationAlert.jsx @@ -9,10 +9,13 @@ import { Icon, } from '@edx/paragon'; import { Check, ArrowForward } from '@edx/paragon/icons'; -import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { sendActivationEmail } from '../../courseware/data'; +import messages from './messages'; -function AccountActivationAlert() { +function AccountActivationAlert({ + intl, +}) { const [showModal, setShowModal] = useState(false); const [showSpinner, setShowSpinner] = useState(false); const [showCheck, setShowCheck] = useState(false); @@ -29,22 +32,12 @@ function AccountActivationAlert() { if (showAccountActivationAlert !== undefined) { Cookies.remove('show-account-activation-popup', { path: '/', domain: process.env.SESSION_COOKIE_DOMAIN }); // extra check to make sure cookie was removed before updating the state. Updating the state without removal - // of cookie would make it infinit rendering + // of cookie would make it infinite rendering if (Cookies.get('show-account-activation-popup') === undefined) { setShowModal(true); } } - const title = ( -

- -

- ); - const button = ( - - )} {/** [MM-P2P] Experiment (className for optimizely trigger) */}
@@ -172,20 +159,23 @@ function OutlineTab({ intl }) { )} - {!courseGoalToDisplay && goalOptions && goalOptions.length > 0 && ( + {!deprecatedCourseGoalToDisplay && goalOptions && goalOptions.length > 0 && ( { setCourseGoalToDisplay(newGoal); }} + setGoalToDisplay={(newGoal) => { setDeprecatedCourseGoalToDisplay(newGoal); }} setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }} /> )} + {resumeCourseUrl && ( + + )} {rootCourseId && ( <>
-
+
@@ -211,15 +201,21 @@ function OutlineTab({ intl }) { courseId={courseId} username={username} /> - {courseGoalToDisplay && goalOptions && goalOptions.length > 0 && ( + {deprecatedCourseGoalToDisplay && goalOptions && goalOptions.length > 0 && ( { setCourseGoalToDisplay(newGoal); }} + selectedGoal={deprecatedCourseGoalToDisplay} + setGoalToDisplay={(newGoal) => { setDeprecatedCourseGoalToDisplay(newGoal); }} setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }} /> )} + {weeklyLearningGoalEnabled && ( + + )} diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx index 52c77533..a539c085 100644 --- a/src/course-home/outline-tab/OutlineTab.test.jsx +++ b/src/course-home/outline-tab/OutlineTab.test.jsx @@ -6,6 +6,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import MockAdapter from 'axios-mock-adapter'; import Cookies from 'js-cookie'; import userEvent from '@testing-library/user-event'; +import messages from './messages'; import { buildMinimalCourseBlocks } from '../../shared/data/__factories__/courseBlocks.factory'; import { @@ -73,7 +74,7 @@ describe('Outline Tab', () => { describe('Course Outline', () => { it('displays link to start course', async () => { await fetchAndRender(); - expect(screen.getByRole('link', { name: 'Start Course' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: messages.start.defaultMessage })).toBeInTheDocument(); }); it('displays link to resume course', async () => { @@ -413,6 +414,111 @@ describe('Outline Tab', () => { }); }); + describe('Start or Resume Course Card', () => { + it('renders startOrResumeCourseCard', async () => { + await fetchAndRender(); + expect(screen.queryByTestId('start-resume-card')).toBeInTheDocument(); + }); + }); + + describe('Weekly Learning Goal', () => { + it('does not render weekly learning goal if weeklyLearningGoalEnabled is false', async () => { + await fetchAndRender(); + expect(screen.queryByTestId('weekly-learning-goal-card')).not.toBeInTheDocument(); + }); + + describe('weekly learning goal is not set', () => { + beforeEach(async () => { + setTabData({ + course_goals: { + weekly_learning_goal_enabled: true, + }, + }); + await fetchAndRender(); + }); + + it('renders weekly learning goal card', async () => { + expect(screen.queryByTestId('weekly-learning-goal-card')).toBeInTheDocument(); + }); + + it('disables the subscribe button if no goal is set', async () => { + expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeDisabled(); + }); + + it('does not show the deprecated goals feature if WeeklyLearningGoal is enabled', async () => { + expect(screen.queryByTestId('course-goal-card')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Goal')).not.toBeInTheDocument(); + expect(screen.queryByTestId('edit-goal-selector')).not.toBeInTheDocument(); + }); + + it.each` + level | days + ${'casual'} | ${1} + ${'regular'} | ${3} + ${'intense'} | ${5} + `('calls the API with a goal of $days when $level goal is clicked', async ({ level, days }) => { + // click on Casual goal + const button = await screen.queryByTestId(`weekly-learning-goal-input-${level}`); + fireEvent.click(button); + // Verify the request was made + await waitFor(() => { + expect(axiosMock.history.post[0].url).toMatch(goalUrl); + // subscribe is turned on automatically + expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${courseId}","days_per_week":${days},"subscribed_to_reminders":true}`); + // verify that the additional info about subscriptions shows up + expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).toBeInTheDocument(); + }); + expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeEnabled(); +}); + it('shows and hides subscribe to reminders additional text', async () => { + const button = await screen.getByTestId('weekly-learning-goal-input-regular'); + fireEvent.click(button); + // Verify the request was made + await waitFor(() => { + expect(axiosMock.history.post[0].url).toMatch(goalUrl); + // subscribe is turned on automatically + expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${courseId}","days_per_week":3,"subscribed_to_reminders":true}`); + // verify that the additional info about subscriptions shows up + expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).toBeInTheDocument(); + }); + expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeEnabled(); + + // Click on subscribe to reminders toggle + const subscriptionSwitch = await screen.getByRole('switch', { name: messages.setGoalReminder.defaultMessage }); + expect(subscriptionSwitch).toBeInTheDocument(); + + fireEvent.click(subscriptionSwitch); + await waitFor(() => { + expect(axiosMock.history.post[1].url).toMatch(goalUrl); + expect(axiosMock.history.post[1].data) + .toMatch(`{"course_id":"${courseId}","days_per_week":3,"subscribed_to_reminders":false}`); + }); + + // verify that the additional info about subscriptions gets hidden + expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).not.toBeInTheDocument(); + }); + }); + describe('weekly learning goal is already set', () => { + beforeEach(async () => { + setTabData({ + course_goals: { + weekly_learning_goal_enabled: true, + selected_goal: { + subscribed_to_reminders: true, + days_per_week: 3, + }, + }, + }); + await fetchAndRender(); + }); + + it('has button for weekly learning goal selected', async () => { + const radio = await screen.queryByTestId('weekly-learning-goal-input-regular'); + expect(radio.checked).toEqual(true); + }); + }); + }); + describe('Course Handouts', () => { it('renders title when handouts are available', async () => { await fetchAndRender(); @@ -856,7 +962,7 @@ describe('Outline Tab', () => { ], }); await fetchAndRender(); - expect(screen.getByRole('link', { name: 'Start Course' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: messages.start.defaultMessage })).toBeInTheDocument(); expect(screen.queryByText('More content is coming soon!')).not.toBeInTheDocument(); }); }); diff --git a/src/course-home/outline-tab/messages.js b/src/course-home/outline-tab/messages.js index 0a2de618..f52e3e5a 100644 --- a/src/course-home/outline-tab/messages.js +++ b/src/course-home/outline-tab/messages.js @@ -5,6 +5,20 @@ const messages = defineMessages({ id: 'learning.outline.dates.all', defaultMessage: 'View all course dates', }, + casualGoalButtonText: { + id: 'learning.outline.goalButton.casual.text', + defaultMessage: '1 day a week', + }, + casualGoalButtonTitle: { + id: 'learning.outline.goalButton.screenReader.text', + defaultMessage: 'Casual', + description: 'A very short description of the least intense of three learning goals', + }, + certAlt: { + id: 'learning.outline.certificateAlt', + defaultMessage: 'Example Certificate', + description: 'Alternate text displayed when the example certificate image cannot be displayed.', + }, collapseAll: { id: 'learning.outline.collapseAll', defaultMessage: 'Collapse all', @@ -39,6 +53,10 @@ const messages = defineMessages({ defaultMessage: 'Goal', description: 'Label for the selected course goal', }, + goalReminderDetail: { + id: 'learning.outline.goalReminderDetail', + defaultMessage: 'If we notice you’re not quite at your goal, we’ll send you an email reminder.', + }, goalUnsure: { id: 'learning.outline.goalUnsure', defaultMessage: 'Not sure yet', @@ -57,6 +75,15 @@ const messages = defineMessages({ defaultMessage: 'Incomplete section', description: 'Text used to describe the gray checkmark icon in front of a section title', }, + intenseGoalButtonText: { + id: 'learning.outline.goalButton.intense.text', + defaultMessage: '5 days a week', + }, + intenseGoalButtonTitle: { + id: 'learning.outline.goalButton.intense.title', + defaultMessage: 'Intense', + description: 'A very short description of the most intensive option of three learning goals, Casual, Regular and Intense', + }, learnMore: { id: 'learning.outline.learnMore', defaultMessage: 'Learn More', @@ -66,6 +93,24 @@ const messages = defineMessages({ defaultMessage: 'Open', description: 'A button to open the given section of the course outline', }, + proctoringInfoPanel: { + id: 'learning.proctoringPanel.header', + defaultMessage: 'This course contains proctored exams', + }, + regularGoalButtonText: { + id: 'learning.outline.goalButton.regular.text', + defaultMessage: '3 days a week', + }, + regularGoalButtonTitle: { + id: 'learning.outline.goalButton.regular.title', + defaultMessage: 'Regular', + description: 'A very short description of the middle option of three learning goals, Casual, Regular and Intense', + }, + resumeBlurb: { + id: 'learning.outline.resumeBlurb', + defaultMessage: 'Pick up where you left off', + description: 'Text describing to the learner that they can return to the last section they visited within the course.', + }, resume: { id: 'learning.outline.resume', defaultMessage: 'Resume course', @@ -74,9 +119,30 @@ const messages = defineMessages({ id: 'learning.outline.setGoal', defaultMessage: 'To start, set a course goal by selecting the option below that best describes your learning plan.', }, + setGoalReminder: { + id: 'learning.outline.setGoalReminder', + defaultMessage: 'Set a goal reminder', + }, + setLearningGoalButtonScreenReaderText: { + id: 'learning.outline.goalButton.casual.title', + defaultMessage: 'Set a learning goal style.', + description: 'screen reader text informing learner they can select an intensity of learning goal', + }, + setWeeklyGoal: { + id: 'learning.outline.setWeeklyGoal', + defaultMessage: 'Set a weekly learning goal', + }, + setWeeklyGoalDetail: { + id: 'learning.outline.setWeeklyGoalDetail', + defaultMessage: 'Setting a goal motivates you to finish the course. You can always change it later.', + }, start: { id: 'learning.outline.start', - defaultMessage: 'Start Course', + defaultMessage: 'Start course', + }, + startBlurb: { + id: 'learning.outline.startBlurb', + defaultMessage: 'Begin your course today', }, tools: { id: 'learning.outline.tools', @@ -90,11 +156,6 @@ const messages = defineMessages({ id: 'learning.outline.upgradeTitle', defaultMessage: 'Pursue a verified certificate', }, - certAlt: { - id: 'learning.outline.certificateAlt', - defaultMessage: 'Example Certificate', - description: 'Alternate text displayed when the example certificate image cannot be displayed.', - }, welcomeMessage: { id: 'learning.outline.welcomeMessage', defaultMessage: 'Welcome Message', @@ -112,10 +173,6 @@ const messages = defineMessages({ defaultMessage: 'Welcome to', description: 'This precedes the title of the course', }, - proctoringInfoPanel: { - id: 'learning.proctoringPanel.header', - defaultMessage: 'This course contains proctored exams', - }, notStartedProctoringStatus: { id: 'learning.proctoringPanel.status.notStarted', defaultMessage: 'Not Started', diff --git a/src/course-home/outline-tab/widgets/CourseGoalCard.jsx b/src/course-home/outline-tab/widgets/DeprecatedCourseGoalCard.jsx similarity index 90% rename from src/course-home/outline-tab/widgets/CourseGoalCard.jsx rename to src/course-home/outline-tab/widgets/DeprecatedCourseGoalCard.jsx index 6d724e39..de09016b 100644 --- a/src/course-home/outline-tab/widgets/CourseGoalCard.jsx +++ b/src/course-home/outline-tab/widgets/DeprecatedCourseGoalCard.jsx @@ -6,9 +6,9 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import messages from '../messages'; -import { saveCourseGoal } from '../../data'; +import { deprecatedSaveCourseGoal } from '../../data'; -function CourseGoalCard({ +function DeprecatedCourseGoalCard({ courseId, goalOptions, intl, @@ -22,7 +22,7 @@ function CourseGoalCard({ text: event.currentTarget.getAttribute('data-goal-text'), }; - saveCourseGoal(courseId, selectedGoal.key).then((response) => { + deprecatedSaveCourseGoal(courseId, selectedGoal.key).then((response) => { const { data } = response; const { header, @@ -80,7 +80,7 @@ function CourseGoalCard({ ); } -CourseGoalCard.propTypes = { +DeprecatedCourseGoalCard.propTypes = { courseId: PropTypes.string.isRequired, goalOptions: PropTypes.arrayOf( PropTypes.arrayOf(PropTypes.string), @@ -91,4 +91,4 @@ CourseGoalCard.propTypes = { setGoalToastHeader: PropTypes.func.isRequired, }; -export default injectIntl(CourseGoalCard); +export default injectIntl(DeprecatedCourseGoalCard); diff --git a/src/course-home/outline-tab/widgets/FlagButton.jsx b/src/course-home/outline-tab/widgets/FlagButton.jsx new file mode 100644 index 00000000..30deff76 --- /dev/null +++ b/src/course-home/outline-tab/widgets/FlagButton.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +function FlagButton({ + buttonIcon, + title, + text, +}) { + return ( +
+
+ {buttonIcon} +
+
+ {title} +
+
+ {text} +
+
+ ); +} + +FlagButton.propTypes = { + buttonIcon: PropTypes.element.isRequired, + title: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, +}; + +export default FlagButton; diff --git a/src/course-home/outline-tab/widgets/FlagButton.scss b/src/course-home/outline-tab/widgets/FlagButton.scss new file mode 100644 index 00000000..49b35240 --- /dev/null +++ b/src/course-home/outline-tab/widgets/FlagButton.scss @@ -0,0 +1,25 @@ +@import "~@edx/brand/paragon/variables"; +@import "~@edx/paragon/scss/core/core"; +@import "~@edx/brand/paragon/overrides"; + +.flag-button { + background-color: white; + border: 2px solid $light-400; + border-radius:.2rem; + + + &:focus { + border: 2px $blue; + box-shadow: 2px solid $yellow; + } + + &:hover { + border: 2px solid $green; + box-shadow: 2px solid $teal; + } +} + + input[type=radio]:checked + .flag-button { + border: 2px solid $red; + box-shadow: 2px solid $green; +} diff --git a/src/course-home/outline-tab/widgets/LearningGoalButton.jsx b/src/course-home/outline-tab/widgets/LearningGoalButton.jsx new file mode 100644 index 00000000..77f5043d --- /dev/null +++ b/src/course-home/outline-tab/widgets/LearningGoalButton.jsx @@ -0,0 +1,83 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { ReactComponent as FlagIntenseIcon } from '@edx/paragon/icons/svg/flag.svg'; +import { useMediaQuery } from 'react-responsive'; +import classNames from 'classnames'; +import { ReactComponent as FlagCasualIcon } from './flag_outline.svg'; +import { ReactComponent as FlagRegularIcon } from './flag_gray.svg'; +import FlagButton from './FlagButton'; +import messages from '../messages'; + +function LearningGoalButton({ + level, + currentGoal, + handleSelect, + intl, +}) { + /* This is not the standard XL media query */ + const isPastXl = useMediaQuery({ query: '(min-width: 1225px)' }); + + const buttonDetails = { + casual: { + daysPerWeek: 1, + title: messages.casualGoalButtonTitle, + text: messages.casualGoalButtonText, + icon: , + }, + regular: { + daysPerWeek: 3, + title: messages.regularGoalButtonTitle, + text: messages.regularGoalButtonText, + icon: , + }, + intense: { + daysPerWeek: 5, + title: messages.intenseGoalButtonTitle, + text: messages.intenseGoalButtonText, + icon: , + }, + }; + + const values = buttonDetails[level]; + + return ( + + ); +} + +LearningGoalButton.propTypes = { + level: PropTypes.string.isRequired, + currentGoal: PropTypes.number.isRequired, + handleSelect: PropTypes.func.isRequired, + intl: intlShape.isRequired, +}; + +export default injectIntl(LearningGoalButton); diff --git a/src/course-home/outline-tab/widgets/StartOrResumeCourseCard.jsx b/src/course-home/outline-tab/widgets/StartOrResumeCourseCard.jsx new file mode 100644 index 00000000..3269741c --- /dev/null +++ b/src/course-home/outline-tab/widgets/StartOrResumeCourseCard.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Button, Card } from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + +import { useSelector } from 'react-redux'; +import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; +import messages from '../messages'; +import { useModel } from '../../../generic/model-store'; + +function StartOrResumeCourseCard({ intl }) { + const { + courseId, + } = useSelector(state => state.courseHome); + + const { + org, + } = useModel('courseHomeMeta', courseId); + + const eventProperties = { + org_key: org, + courserun_key: courseId, + }; + + const { + resumeCourse: { + hasVisitedCourse, + url: resumeCourseUrl, + }, + + } = useModel('outline', courseId); + + const logResumeCourseClick = () => { + sendTrackingLogEvent('edx.course.home.resume_course.clicked', { + ...eventProperties, + event_type: hasVisitedCourse ? 'resume' : 'start', + url: resumeCourseUrl, + }); + }; + + return ( + + +
+

{intl.formatMessage(messages.startBlurb)}

+
+ +
+
+
+
+ ); +} + +StartOrResumeCourseCard.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(StartOrResumeCourseCard); diff --git a/src/course-home/outline-tab/widgets/UpdateGoalSelector.jsx b/src/course-home/outline-tab/widgets/UpdateGoalSelector.jsx index b8729307..38c593ea 100644 --- a/src/course-home/outline-tab/widgets/UpdateGoalSelector.jsx +++ b/src/course-home/outline-tab/widgets/UpdateGoalSelector.jsx @@ -5,7 +5,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Dropdown } from '@edx/paragon'; import messages from '../messages'; -import { saveCourseGoal } from '../../data'; +import { deprecatedSaveCourseGoal } from '../../data'; function UpdateGoalSelector({ courseId, @@ -24,7 +24,7 @@ function UpdateGoalSelector({ }; setGoalToDisplay(newGoal); - saveCourseGoal(courseId, key).then((response) => { + deprecatedSaveCourseGoal(courseId, key).then((response) => { const { data } = response; const { header, diff --git a/src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx b/src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx new file mode 100644 index 00000000..65f45da7 --- /dev/null +++ b/src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx @@ -0,0 +1,109 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; + +import { Form, Card, Icon } from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Email } from '@edx/paragon/icons'; +import { useSelector } from 'react-redux'; +import messages from '../messages'; +import LearningGoalButton from './LearningGoalButton'; +import { saveWeeklyLearningGoal } from '../../data'; + +function WeeklyLearningGoalCard({ + daysPerWeek, + subscribedToReminders, + intl, +}) { + const { + courseId, + } = useSelector(state => state.courseHome); + + const [daysPerWeekGoal, setDaysPerWeekGoal] = useState(daysPerWeek); + // eslint-disable-next-line react/prop-types + const [isGetReminderSelected, setGetReminderSelected] = useState(subscribedToReminders); + + function handleSelect(days) { + // Set the subscription button if this is the first time selecting a goal + const selectReminders = daysPerWeekGoal === null ? true : isGetReminderSelected; + setGetReminderSelected(selectReminders); + setDaysPerWeekGoal(days); + saveWeeklyLearningGoal(courseId, days, selectReminders); + } + + function handleSubscribeToReminders(event) { + const isGetReminderChecked = event.target.checked; + setGetReminderSelected(isGetReminderChecked); + saveWeeklyLearningGoal(courseId, daysPerWeekGoal, isGetReminderChecked); + } + + return ( +
+ + + +

{intl.formatMessage(messages.setWeeklyGoal)}

+
+ + {intl.formatMessage(messages.setWeeklyGoalDetail)} + +
+ + + +
+
+ handleSubscribeToReminders(event)} + disabled={!daysPerWeekGoal} + > + {intl.formatMessage(messages.setGoalReminder)} + +
+
+ {isGetReminderSelected && ( + +
+
+ +
+
+ {intl.formatMessage(messages.goalReminderDetail)} +
+
+
+ )} +
+ +
+ ); +} + +WeeklyLearningGoalCard.propTypes = { + daysPerWeek: PropTypes.number, + subscribedToReminders: PropTypes.bool, + intl: intlShape.isRequired, +}; + +WeeklyLearningGoalCard.defaultProps = { + daysPerWeek: null, + subscribedToReminders: false, +}; +export default injectIntl(WeeklyLearningGoalCard); diff --git a/src/course-home/outline-tab/widgets/flag_gray.svg b/src/course-home/outline-tab/widgets/flag_gray.svg new file mode 100644 index 00000000..4ae7e147 --- /dev/null +++ b/src/course-home/outline-tab/widgets/flag_gray.svg @@ -0,0 +1,18 @@ + + + + + diff --git a/src/course-home/outline-tab/widgets/flag_outline.svg b/src/course-home/outline-tab/widgets/flag_outline.svg new file mode 100644 index 00000000..78d1af24 --- /dev/null +++ b/src/course-home/outline-tab/widgets/flag_outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/index.scss b/src/index.scss index 7ecf9830..f3834221 100755 --- a/src/index.scss +++ b/src/index.scss @@ -391,6 +391,7 @@ @import "course-home/dates-tab/timeline/Day.scss"; @import "generic/upgrade-notification/UpgradeNotification.scss"; @import "course-home/outline-tab/widgets/ProctoringInfoPanel.scss"; +@import "src/course-home/outline-tab/widgets/FlagButton.scss"; @import "course-home/progress-tab/course-completion/CompletionDonutChart.scss"; @import "course-home/progress-tab/grades/course-grade/GradeBar.scss"; @import "courseware/course/course-exit/CourseRecommendations";