Compare commits

...

29 Commits

Author SHA1 Message Date
Chris Deery
6c9899be5e feat: [AA-906] A11y changes for buttons to be seen as radio buttons 2021-10-01 14:35:20 -04:00
Chris Deery
93873cd845 feat: [AA-906] A11y changes for buttons to be seen as radio buttons 2021-10-01 14:22:58 -04:00
Chris Deery
3def37d28c feat: [AA-906] Merge conflicts with Master 2021-09-30 13:48:10 -04:00
Chris Deery
d7e7b8c873 feat: [AA-906] Additional tests 2021-09-30 11:56:40 -04:00
Chris Deery
b401f7bb34 feat: [AA-906] missed one review comment 2021-09-30 11:56:40 -04:00
Chris Deery
952d5d89e7 feat: [AA-906] Cleanup and add some Unit tests 2021-09-30 11:56:40 -04:00
Chris Deery
15d9c9f1bd feat: [AA-906] rebase from master 2021-09-30 11:56:19 -04:00
Chris Deery
b5e22c983d feat: [AA-906] update snapshot 2021-09-30 11:52:55 -04:00
Chris Deery
4d3b68d287 feat: [AA-906] implemented review feedback 2021-09-30 11:52:55 -04:00
Chris Deery
2607cec626 feat: [AA-906] updated redux.test.js.snap with new data for goals 2021-09-30 11:52:55 -04:00
Chris Deery
ec18af1ec0 feat: [AA-906] Front end for Number of Days goal setting
fix errors from AccountActivationAlert.jsx
2021-09-30 11:52:55 -04:00
Chris Deery
ee62aab0e7 feat: [AA-906] Front end for Number of Days goal setting
fix undefined access
2021-09-30 11:52:55 -04:00
Chris Deery
3cb55b320a feat: [AA-906] Front end for Number of Days goal setting
remove console.log calls
2021-09-30 11:52:55 -04:00
Chris Deery
d2e26bf8cf feat: [AA-906] Front end for Number of Days goal setting
fix improper I18n messages
2021-09-30 11:52:55 -04:00
Chris Deery
95fb4ec859 feat: [AA-906] Front end for Number of Days goal setting
Implement days per week buttons
link buttons to api to set value
Implement subscribe button
link subscribe button to api
tweak CSS
Add new icons for flags
Add new function for updating weekly goals
2021-09-30 11:52:55 -04:00
Chris Deery
03f0ec4aec feat: [AA-906] Front end for Number of Days goal setting
Consume flag for course_goals[number_of_days_goals_enabled]
Added StartResumeCard to outlineTab
Move the Start/Resume course button to above the Intro
Put the start/resume in a card with a text block
2021-09-30 11:52:55 -04:00
Chris Deery
9dc447fa85 feat: [AA-906] Additional tests 2021-09-29 09:41:50 -04:00
Chris Deery
0286304c39 feat: [AA-906] missed one review comment 2021-09-27 09:02:15 -04:00
Chris Deery
d88c4ade78 feat: [AA-906] Cleanup and add some Unit tests 2021-09-27 08:43:09 -04:00
Chris Deery
e3b8143677 feat: [AA-906] more review updates and refactoring 2021-09-21 17:29:10 -04:00
Chris Deery
09384b317e feat: [AA-906] update snapshot 2021-09-21 15:15:03 -04:00
Chris Deery
2172ea4041 feat: [AA-906] implemented review feedback 2021-09-21 15:02:26 -04:00
Chris Deery
7e3aee7147 feat: [AA-906] updated redux.test.js.snap with new data for goals 2021-09-20 15:29:38 -04:00
Chris Deery
4f187390a0 feat: [AA-906] Front end for Number of Days goal setting
fix errors from AccountActivationAlert.jsx
2021-09-20 15:10:44 -04:00
Chris Deery
5b3053d3bb feat: [AA-906] Front end for Number of Days goal setting
fix undefined access
2021-09-17 17:04:30 -04:00
Chris Deery
dae1e03e73 feat: [AA-906] Front end for Number of Days goal setting
remove console.log calls
2021-09-17 12:03:51 -04:00
Chris Deery
a32c79984d feat: [AA-906] Front end for Number of Days goal setting
fix improper I18n messages
2021-09-17 11:51:34 -04:00
Chris Deery
8d0596a32e feat: [AA-906] Front end for Number of Days goal setting
Implement days per week buttons
link buttons to api to set value
Implement subscribe button
link subscribe button to api
tweak CSS
Add new icons for flags
Add new function for updating weekly goals
2021-09-17 11:40:12 -04:00
Chris Deery
91b1229a29 feat: [AA-906] Front end for Number of Days goal setting
Consume flag for course_goals[number_of_days_goals_enabled]
Added StartResumeCard to outlineTab
Move the Start/Resume course button to above the Intro
Put the start/resume in a card with a text block
2021-09-09 09:56:31 -04:00
15 changed files with 499 additions and 38 deletions

View File

@@ -9,10 +9,13 @@ import {
Icon, Icon,
} from '@edx/paragon'; } from '@edx/paragon';
import { Check, ArrowForward } from '@edx/paragon/icons'; 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 { sendActivationEmail } from '../../courseware/data';
import messages from './messages';
function AccountActivationAlert() { function AccountActivationAlert({
intl,
}) {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [showSpinner, setShowSpinner] = useState(false); const [showSpinner, setShowSpinner] = useState(false);
const [showCheck, setShowCheck] = useState(false); const [showCheck, setShowCheck] = useState(false);
@@ -29,22 +32,12 @@ function AccountActivationAlert() {
if (showAccountActivationAlert !== undefined) { if (showAccountActivationAlert !== undefined) {
Cookies.remove('show-account-activation-popup', { path: '/', domain: process.env.SESSION_COOKIE_DOMAIN }); 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 // 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) { if (Cookies.get('show-account-activation-popup') === undefined) {
setShowModal(true); setShowModal(true);
} }
} }
const title = (
<h3>
<FormattedMessage
id="account-activation.alert.title"
defaultMessage="Activate your account so you can log back in"
description="Title for account activation alert which is shown after the registration"
/>
</h3>
);
const button = ( const button = (
<Button <Button
variant="primary" variant="primary"
@@ -64,7 +57,7 @@ function AccountActivationAlert() {
); );
const children = () => { const children = () => {
let bodyContent = null; let bodyContent;
const message = ( const message = (
<FormattedMessage <FormattedMessage
id="account-activation.alert.message" id="account-activation.alert.message"
@@ -123,7 +116,7 @@ function AccountActivationAlert() {
return ( return (
<AlertModal <AlertModal
isOpen={showModal} isOpen={showModal}
title={title} title={intl.formatMessage(messages.accountActivationAlertTitle)}
footerNode={button} footerNode={button}
onClose={() => ({})} onClose={() => ({})}
> >
@@ -132,4 +125,8 @@ function AccountActivationAlert() {
); );
} }
AccountActivationAlert.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(AccountActivationAlert); export default injectIntl(AccountActivationAlert);

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
accountActivationAlertTitle: {
id: 'account-activation.alert.title',
defaultMessage: 'Activate your account so you can log back in',
description: 'Title for account activation alert which is shown after the registration',
},
});
export default messages;

View File

@@ -40,6 +40,9 @@ Factory.define('outlineTabData')
course_goals: { course_goals: {
goal_options: [], goal_options: [],
selected_goal: null, selected_goal: null,
number_of_days_goals_enabled: false,
days_per_week: null,
subscribed_to_reminders: null,
}, },
course_tools: [ course_tools: [
{ {

View File

@@ -432,8 +432,11 @@ Object {
}, },
}, },
"courseGoals": Object { "courseGoals": Object {
"daysPerWeek": null,
"goalOptions": Array [], "goalOptions": Array [],
"numberOfDaysGoalsEnabled": false,
"selectedGoal": null, "selectedGoal": null,
"subscribedToReminders": null,
}, },
"courseTools": Array [ "courseTools": Array [
Object { Object {

View File

@@ -391,6 +391,15 @@ export async function postCourseGoals(courseId, goalKey) {
return getAuthenticatedHttpClient().post(url.href, { course_id: courseId, goal_key: goalKey }); return getAuthenticatedHttpClient().post(url.href, { course_id: courseId, goal_key: goalKey });
} }
export async function postWeeklyCourseGoals(courseId, daysPerWeek, subscribedToReminders) {
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`);
return getAuthenticatedHttpClient().post(url.href, {
course_id: courseId,
days_per_week: daysPerWeek,
subscribed_to_reminders: subscribedToReminders,
});
}
export async function postDismissWelcomeMessage(courseId) { export async function postDismissWelcomeMessage(courseId) {
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`); const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`);
await getAuthenticatedHttpClient().post(url.href, { course_id: courseId }); await getAuthenticatedHttpClient().post(url.href, { course_id: courseId });

View File

@@ -4,6 +4,7 @@ export {
fetchProgressTab, fetchProgressTab,
resetDeadlines, resetDeadlines,
saveCourseGoal, saveCourseGoal,
saveWeeklyCourseGoal,
} from './thunks'; } from './thunks';
export { reducer } from './slice'; export { reducer } from './slice';

View File

@@ -8,6 +8,7 @@ import {
getProgressTabData, getProgressTabData,
postCourseDeadlines, postCourseDeadlines,
postCourseGoals, postCourseGoals,
postWeeklyCourseGoals,
postDismissWelcomeMessage, postDismissWelcomeMessage,
postRequestCert, postRequestCert,
} from './api'; } from './api';
@@ -113,6 +114,10 @@ export async function saveCourseGoal(courseId, goalKey) {
return postCourseGoals(courseId, goalKey); return postCourseGoals(courseId, goalKey);
} }
export async function saveWeeklyCourseGoal(courseId, daysPerWeek, subscribedToReminders) {
return postWeeklyCourseGoals(courseId, daysPerWeek, subscribedToReminders);
}
export function processEvent(eventData, getTabData) { export function processEvent(eventData, getTabData) {
return async (dispatch) => { return async (dispatch) => {
// Pulling this out early so the data doesn't get camelCased and is easier // Pulling this out early so the data doesn't get camelCased and is easier

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -10,6 +10,8 @@ import { AlertList } from '../../generic/user-messages';
import CourseDates from './widgets/CourseDates'; import CourseDates from './widgets/CourseDates';
import CourseGoalCard from './widgets/CourseGoalCard'; import CourseGoalCard from './widgets/CourseGoalCard';
import CourseHandouts from './widgets/CourseHandouts'; import CourseHandouts from './widgets/CourseHandouts';
import StartOrResumeCourseCard from './widgets/StartOrResumeCourseCard';
import WeeklyLearningGoalCard from './widgets/WeeklyLearningGoalCard';
import CourseTools from './widgets/CourseTools'; import CourseTools from './widgets/CourseTools';
import { fetchOutlineTab } from '../data'; import { fetchOutlineTab } from '../data';
import genericMessages from '../../generic/messages'; import genericMessages from '../../generic/messages';
@@ -54,13 +56,13 @@ function OutlineTab({ intl }) {
courseGoals: { courseGoals: {
goalOptions, goalOptions,
selectedGoal, selectedGoal,
weeklyLearningGoalEnabled,
} = {}, } = {},
datesBannerInfo, datesBannerInfo,
datesWidget: { datesWidget: {
courseDateBlocks, courseDateBlocks,
}, },
resumeCourse: { resumeCourse: {
hasVisitedCourse,
url: resumeCourseUrl, url: resumeCourseUrl,
}, },
offer, offer,
@@ -68,7 +70,7 @@ function OutlineTab({ intl }) {
verifiedMode, verifiedMode,
} = useModel('outline', courseId); } = useModel('outline', courseId);
const [courseGoalToDisplay, setCourseGoalToDisplay] = useState(selectedGoal); const [deprecatedCourseGoalToDisplay, setDeprecatedCourseGoalToDisplay] = useState(selectedGoal);
const [goalToastHeader, setGoalToastHeader] = useState(''); const [goalToastHeader, setGoalToastHeader] = useState('');
const [expandAll, setExpandAll] = useState(false); const [expandAll, setExpandAll] = useState(false);
@@ -77,14 +79,6 @@ function OutlineTab({ intl }) {
courserun_key: courseId, courserun_key: courseId,
}; };
const logResumeCourseClick = () => {
sendTrackingLogEvent('edx.course.home.resume_course.clicked', {
...eventProperties,
event_type: hasVisitedCourse ? 'resume' : 'start',
url: resumeCourseUrl,
});
};
// Below the course title alerts (appearing in the order listed here) // Below the course title alerts (appearing in the order listed here)
const courseStartAlert = useCourseStartAlert(courseId); const courseStartAlert = useCourseStartAlert(courseId);
const courseEndAlert = useCourseEndAlert(courseId); const courseEndAlert = useCourseEndAlert(courseId);
@@ -132,13 +126,6 @@ function OutlineTab({ intl }) {
<div className="col-12 col-sm-auto p-0"> <div className="col-12 col-sm-auto p-0">
<div role="heading" aria-level="1" className="h2">{title}</div> <div role="heading" aria-level="1" className="h2">{title}</div>
</div> </div>
{resumeCourseUrl && (
<div className="col-12 col-sm-auto p-0">
<Button variant="brand" block href={resumeCourseUrl} onClick={() => logResumeCourseClick()}>
{hasVisitedCourse ? intl.formatMessage(messages.resume) : intl.formatMessage(messages.start)}
</Button>
</div>
)}
</div> </div>
{/** [MM-P2P] Experiment (className for optimizely trigger) */} {/** [MM-P2P] Experiment (className for optimizely trigger) */}
<div className="row course-outline-tab"> <div className="row course-outline-tab">
@@ -172,15 +159,18 @@ function OutlineTab({ intl }) {
<UpgradeToShiftDatesAlert model="outline" logUpgradeLinkClick={logUpgradeToShiftDatesLinkClick} /> <UpgradeToShiftDatesAlert model="outline" logUpgradeLinkClick={logUpgradeToShiftDatesLinkClick} />
</> </>
)} )}
{!courseGoalToDisplay && goalOptions && goalOptions.length > 0 && ( {!deprecatedCourseGoalToDisplay && goalOptions && goalOptions.length > 0 && (
<CourseGoalCard <CourseGoalCard
courseId={courseId} courseId={courseId}
goalOptions={goalOptions} goalOptions={goalOptions}
title={title} title={title}
setGoalToDisplay={(newGoal) => { setCourseGoalToDisplay(newGoal); }} setGoalToDisplay={(newGoal) => { setDeprecatedCourseGoalToDisplay(newGoal); }}
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }} setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
/> />
)} )}
{resumeCourseUrl && (
<StartOrResumeCourseCard />
)}
<WelcomeMessage courseId={courseId} /> <WelcomeMessage courseId={courseId} />
{rootCourseId && ( {rootCourseId && (
<> <>
@@ -211,15 +201,22 @@ function OutlineTab({ intl }) {
courseId={courseId} courseId={courseId}
username={username} username={username}
/> />
{courseGoalToDisplay && goalOptions && goalOptions.length > 0 && ( {deprecatedCourseGoalToDisplay && goalOptions && goalOptions.length > 0 && (
<UpdateGoalSelector <UpdateGoalSelector
courseId={courseId} courseId={courseId}
goalOptions={goalOptions} goalOptions={goalOptions}
selectedGoal={courseGoalToDisplay} selectedGoal={deprecatedCourseGoalToDisplay}
setGoalToDisplay={(newGoal) => { setCourseGoalToDisplay(newGoal); }} setGoalToDisplay={(newGoal) => { setDeprecatedCourseGoalToDisplay(newGoal); }}
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }} setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
/> />
)} )}
{weeklyLearningGoalEnabled && (
<WeeklyLearningGoalCard
daysPerWeek={selectedGoal && 'daysPerWeek' in selectedGoal ? selectedGoal.daysPerWeek : null}
subscribedToReminders={selectedGoal && 'subscribedToReminders' in selectedGoal ? selectedGoal.subscribedToReminders : false}
courseId={courseId}
/>
)}
<CourseTools <CourseTools
courseId={courseId} courseId={courseId}
/> />

View File

@@ -6,6 +6,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import messages from './messages';
import { buildMinimalCourseBlocks } from '../../shared/data/__factories__/courseBlocks.factory'; import { buildMinimalCourseBlocks } from '../../shared/data/__factories__/courseBlocks.factory';
import { import {
@@ -413,6 +414,68 @@ describe('Outline Tab', () => {
}); });
}); });
describe('Weekly Learning Goals', () => {
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('renders startOrResumeCourseCard', async () => {
expect(screen.queryByTestId('start-resume-card')).toBeInTheDocument();
});
it('disables the subscribe button if no goal is set', async () => {
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeDisabled();
});
it('calls the API when a button is clicked', async () => {
expect(screen.queryByText(messages.casualGoalButtonText.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.casualGoalButtonText.defaultMessage).closest('button')).toBeInTheDocument();
// click on Casual goal
const button = await screen.getByText(messages.casualGoalButtonText.defaultMessage).closest('button');
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
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('Course Handouts', () => { describe('Course Handouts', () => {
it('renders title when handouts are available', async () => { it('renders title when handouts are available', async () => {
await fetchAndRender(); await fetchAndRender();

View File

@@ -66,6 +66,15 @@ const messages = defineMessages({
defaultMessage: 'Open', defaultMessage: 'Open',
description: 'A button to open the given section of the course outline', description: 'A button to open the given section of the course outline',
}, },
startBlurb: {
id: 'learning.outline.startBlurb',
defaultMessage: 'Begin your course today',
},
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: { resume: {
id: 'learning.outline.resume', id: 'learning.outline.resume',
defaultMessage: 'Resume course', defaultMessage: 'Resume course',
@@ -74,6 +83,14 @@ const messages = defineMessages({
id: 'learning.outline.setGoal', id: 'learning.outline.setGoal',
defaultMessage: 'To start, set a course goal by selecting the option below that best describes your learning plan.', defaultMessage: 'To start, set a course goal by selecting the option below that best describes your learning plan.',
}, },
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: { start: {
id: 'learning.outline.start', id: 'learning.outline.start',
defaultMessage: 'Start Course', defaultMessage: 'Start Course',
@@ -112,6 +129,46 @@ const messages = defineMessages({
defaultMessage: 'Welcome to', defaultMessage: 'Welcome to',
description: 'This precedes the title of the course', description: 'This precedes the title of the course',
}, },
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',
},
casualGoalButtonTitle: {
id: 'learning.outline.goalButton.screenReader.text',
defaultMessage: 'Casual',
description: 'A very short description of the least intense of three learning goals',
},
casualGoalButtonText: {
id: 'learning.outline.goalButton.casual.text',
defaultMessage: '1 day 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',
},
regularGoalButtonText: {
id: 'learning.outline.goalButton.regular.text',
defaultMessage: '3 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',
},
intenseGoalButtonText: {
id: 'learning.outline.goalButton.intense.text',
defaultMessage: '5 days a week',
},
setGoalReminder: {
id: 'learning.outline.setGoalReminder',
defaultMessage: 'Set a goal reminder',
},
goalReminderDetail: {
id: 'learning.outline.goalReminderDetail',
defaultMessage: 'If we notice youre not quite at your goal, well send you an email reminder.',
},
proctoringInfoPanel: { proctoringInfoPanel: {
id: 'learning.proctoringPanel.header', id: 'learning.proctoringPanel.header',
defaultMessage: 'This course contains proctored exams', defaultMessage: 'This course contains proctored exams',
@@ -220,6 +277,11 @@ const messages = defineMessages({
id: 'learning.proctoringPanel.onboardingButtonPastDue', id: 'learning.proctoringPanel.onboardingButtonPastDue',
defaultMessage: 'Onboarding Past Due', defaultMessage: 'Onboarding Past Due',
}, },
accountActivationAlertTitle: {
id: 'account-activation.alert.title',
defaultMessage: 'Activate your account so you can log back in',
description: 'Title for account activation alert which is shown after the registration',
},
}); });
export default messages; export default messages;

View File

@@ -0,0 +1,51 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
function FlagButton({
ButtonIcon,
srText,
title,
text,
isEnabled,
handleSelect,
}) {
const [isSelected, setIsSelected] = useState(false);
return (
<button
type="button"
className={classNames('col flex-grow-1 p-3 border border-light rounded bg-white', { 'border-dark': isEnabled || isSelected })}
onMouseEnter={() => setIsSelected(true)}
onMouseLeave={() => setIsSelected(false)}
onClick={() => handleSelect()}
>
<div className=" justify-content-center">
{ButtonIcon}
</div>
<span className="sr-only sr-only-focusable">{srText}</span>
<div className="text-center small">
{title}
</div>
<div className="text-center micro">
{text}
</div>
</button>
);
}
FlagButton.propTypes = {
ButtonIcon: PropTypes.element.isRequired,
srText: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
text: PropTypes.string,
isEnabled: PropTypes.bool,
handleSelect: PropTypes.func.isRequired,
};
FlagButton.defaultProps = {
isEnabled: false,
text: '',
};
export default FlagButton;

View File

@@ -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 (
<Card className="mb-3" data-testid="start-resume-card">
<Card.Body>
<div className="row w-100 m-0 ">
<h2 className="h4 col-auto flex-grow-1">{intl.formatMessage(messages.startBlurb)}</h2>
<div className="col col-auto p-0 justify-content-end">
<Button
variant="brand"
block
href={resumeCourseUrl}
onClick={() => logResumeCourseClick()}
>
{hasVisitedCourse ? intl.formatMessage(messages.resume) : intl.formatMessage(messages.start)}
</Button>
</div>
</div>
</Card.Body>
</Card>
);
}
StartOrResumeCourseCard.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(StartOrResumeCourseCard);

View File

@@ -0,0 +1,173 @@
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 { ReactComponent as FlagIntenseIcon } from '@edx/paragon/icons/svg/flag.svg';
import { ReactComponent as FlagCasualIcon } from './flag_outline.svg';
import { ReactComponent as FlagRegularIcon } from './flag_gray.svg';
import messages from '../messages';
import FlagButton from './FlagButton';
import { saveWeeklyCourseGoal } from '../../data';
function WeeklyLearningGoalCard({
daysPerWeek,
subscribedToReminders,
courseId,
intl,
}) {
const [daysPerWeekGoal, setDaysPerWeekGoal] = useState(daysPerWeek);
// eslint-disable-next-line react/prop-types
const [isGetReminderSelected, setGetReminderSelected] = useState(subscribedToReminders);
const weeklyLearningGoalLevels = {
CASUAL: 3,
REGULAR: 4,
INTENSE: 5,
};
Object.freeze(weeklyLearningGoalLevels);
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);
saveWeeklyCourseGoal(courseId, days, selectReminders);
}
function handleSubscribeToReminders(event) {
const isGetReminderChecked = event.target.checked;
setGetReminderSelected(isGetReminderChecked);
saveWeeklyCourseGoal(courseId, daysPerWeekGoal, isGetReminderChecked);
}
return (
<div className="row w-100 m-0 p-0">
<Card className="mb-3" data-testid="weekly-learning-goal-card">
<Card.Body>
<Card.Title>
<h4 className="m-0">{intl.formatMessage(messages.setWeeklyGoal)}</h4>
</Card.Title>
<Card.Text>
{intl.formatMessage(messages.setWeeklyGoalDetail)}
</Card.Text>
<div
className="row w-100 m-0 p-0 justify-content-around"
>
<label
htmlFor={weeklyLearningGoalLevels.CASUAL}
className="col-auto col-md-12 col-xl-auto m-0 p-0 pb-md-3 pb-xl-0"
>
<input
type="radio"
id={weeklyLearningGoalLevels.CASUAL}
name="learningGoal"
radioGroup="learningGoal"
value={weeklyLearningGoalLevels.CASUAL}
onChange={() => handleSelect(weeklyLearningGoalLevels.CASUAL)}
tabIndex="0"
checked={weeklyLearningGoalLevels.CASUAL === daysPerWeekGoal}
className="position-absolute invisible"
/>
<FlagButton
ButtonIcon={<FlagCasualIcon />}
srText={intl.formatMessage(messages.setLearningGoalButtonScreenReaderText)}
title={intl.formatMessage(messages.casualGoalButtonTitle)}
text={intl.formatMessage(messages.casualGoalButtonText)}
isEnabled={weeklyLearningGoalLevels.CASUAL === daysPerWeekGoal}
handleSelect={() => { handleSelect(weeklyLearningGoalLevels.CASUAL); }}
/>
</label>
<label
htmlFor={weeklyLearningGoalLevels.REGULAR}
className="col-auto col-md-12 col-xl-auto m-0 p-0 pb-md-3 pb-xl-0"
>
<input
type="radio"
id={weeklyLearningGoalLevels.REGULAR}
name="learningGoal"
radioGroup="learningGoal"
value={weeklyLearningGoalLevels.REGULAR}
onChange={() => handleSelect(weeklyLearningGoalLevels.REGULAR)}
tabIndex="-1"
checked={weeklyLearningGoalLevels.REGULAR === daysPerWeekGoal}
className="position-absolute invisible"
/>
<FlagButton
ButtonIcon={<FlagRegularIcon />}
srText={intl.formatMessage(messages.setLearningGoalButtonScreenReaderText)}
title={intl.formatMessage(messages.regularGoalButtonTitle)}
text={intl.formatMessage(messages.regularGoalButtonText)}
isEnabled={weeklyLearningGoalLevels.REGULAR === daysPerWeekGoal}
handleSelect={() => { handleSelect(weeklyLearningGoalLevels.REGULAR); }}
/>
</label>
<label
htmlFor={weeklyLearningGoalLevels.INTENSE}
className="col-auto col-md-12 col-xl-auto m-0 p-0 pb-md-3 pb-xl-0"
>
<input
type="radio"
id={weeklyLearningGoalLevels.INTENSE}
name="learningGoal"
radioGroup="learningGoal"
value={weeklyLearningGoalLevels.INTENSE}
onChange={() => handleSelect(weeklyLearningGoalLevels.INTENSE)}
tabIndex="-1"
checked={weeklyLearningGoalLevels.INTENSE === daysPerWeekGoal}
className="position-absolute invisible"
/>
<FlagButton
ButtonIcon={<FlagIntenseIcon />}
srText={intl.formatMessage(messages.setLearningGoalButtonScreenReaderText)}
title={intl.formatMessage(messages.intenseGoalButtonTitle)}
text={intl.formatMessage(messages.intenseGoalButtonText)}
isEnabled={weeklyLearningGoalLevels.INTENSE === daysPerWeekGoal}
handleSelect={() => { handleSelect(weeklyLearningGoalLevels.INTENSE); }}
/>
</label>
</div>
<div className="row p-3">
<Form.Switch
checked={isGetReminderSelected}
onChange={(event) => handleSubscribeToReminders(event)}
disabled={!daysPerWeekGoal}
>
{intl.formatMessage(messages.setGoalReminder)}
</Form.Switch>
</div>
</Card.Body>
{isGetReminderSelected && (
<Card.Footer className="border-0 px-2.5">
<div className="row w-100 m-0 small align-center">
<div className="d-flex align-items-center pr-1.5">
<Icon src={Email} />
</div>
<div className="col align-center">
{intl.formatMessage(messages.goalReminderDetail)}
</div>
</div>
</Card.Footer>
)}
</Card>
</div>
);
}
WeeklyLearningGoalCard.propTypes = {
daysPerWeek: PropTypes.number,
subscribedToReminders: PropTypes.bool,
courseId: PropTypes.string.isRequired,
intl: intlShape.isRequired,
};
WeeklyLearningGoalCard.defaultProps = {
daysPerWeek: null,
subscribedToReminders: false,
};
export default injectIntl(WeeklyLearningGoalCard);

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="15"
height="17"
viewBox="0 0 15 17"
fill="none"
version="1.1"
id="svg11"
xmlns="http://www.w3.org/2000/svg">
<path
d="M9.4 2L9 0H0V17H2V10H7.6L8 12H15V2H9.4ZM13 10H9.64L9.24 8H2V2H7.36L7.76 4H13V10Z"
fill="#002B2B"
id="path9" />
<path
style="fill:#808080;fill-rule:evenodd;stroke-width:0.0150977"
d="M 9.6594698,9.9871226 C 9.6577909,9.9829707 9.5654776,9.5311723 9.4543296,8.9831261 L 9.2522415,7.9866785 5.6376662,7.9790074 2.0230906,7.9713362 V 4.9970494 2.0227625 l 2.6636151,0.00771 2.6636151,0.00771 0.1968204,0.9888987 0.1968205,0.9888988 h 2.6200263 2.620026 v 2.9893428 2.9893428 h -1.660746 c -0.91341,0 -1.6621194,-0.0034 -1.6637982,-0.00755 z"
id="path302" />
</svg>

After

Width:  |  Height:  |  Size: 801 B

View File

@@ -0,0 +1,3 @@
<svg width="15" height="17" viewBox="0 0 15 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.4 2L9 0H0V17H2V10H7.6L8 12H15V2H9.4ZM13 10H9.64L9.24 8H2V2H7.36L7.76 4H13V10Z" fill="#002B2B"/>
</svg>

After

Width:  |  Height:  |  Size: 211 B