From d8f3c7441efae4c0e8b4d5abe7f6c057864c932b Mon Sep 17 00:00:00 2001 From: Chris Deery <3932645+cdeery@users.noreply.github.com> Date: Tue, 19 Oct 2021 10:37:22 -0400 Subject: [PATCH] feat: [AA-906] UI for WeeklyLearningGoals (#664) * feat: [AA-906] UI for WeeklyLearningGoals Add component to OutlineTab for selecting Weekly Learning Goals Move start button to before course outline, and put in card with Call to action. Unit tests Implement temporary a11y feedback add react-responsive as a dependency Everything except for the start/resume button move is behind a waffle flag: course_goals.number_of_days_goals --- package-lock.json | 13 +++ package.json | 1 + .../AccountActivationAlert.jsx | 27 ++--- src/alerts/logistration-alert/messages.js | 11 ++ .../__factories__/outlineTabData.factory.js | 3 + .../data/__snapshots__/redux.test.js.snap | 3 + src/course-home/data/api.js | 11 +- src/course-home/data/index.js | 3 +- src/course-home/data/redux.test.js | 2 +- src/course-home/data/thunks.js | 11 +- src/course-home/outline-tab/OutlineTab.jsx | 46 ++++---- .../outline-tab/OutlineTab.test.jsx | 110 +++++++++++++++++- src/course-home/outline-tab/messages.js | 77 ++++++++++-- ...lCard.jsx => DeprecatedCourseGoalCard.jsx} | 10 +- .../outline-tab/widgets/FlagButton.jsx | 32 +++++ .../outline-tab/widgets/FlagButton.scss | 25 ++++ .../widgets/LearningGoalButton.jsx | 83 +++++++++++++ .../widgets/StartOrResumeCourseCard.jsx | 65 +++++++++++ .../widgets/UpdateGoalSelector.jsx | 4 +- .../widgets/WeeklyLearningGoalCard.jsx | 109 +++++++++++++++++ .../outline-tab/widgets/flag_gray.svg | 18 +++ .../outline-tab/widgets/flag_outline.svg | 3 + src/index.scss | 1 + 23 files changed, 603 insertions(+), 65 deletions(-) create mode 100644 src/alerts/logistration-alert/messages.js rename src/course-home/outline-tab/widgets/{CourseGoalCard.jsx => DeprecatedCourseGoalCard.jsx} (90%) create mode 100644 src/course-home/outline-tab/widgets/FlagButton.jsx create mode 100644 src/course-home/outline-tab/widgets/FlagButton.scss create mode 100644 src/course-home/outline-tab/widgets/LearningGoalButton.jsx create mode 100644 src/course-home/outline-tab/widgets/StartOrResumeCourseCard.jsx create mode 100644 src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx create mode 100644 src/course-home/outline-tab/widgets/flag_gray.svg create mode 100644 src/course-home/outline-tab/widgets/flag_outline.svg 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";