Compare commits
29 Commits
master
...
cdeery/AA-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c9899be5e | ||
|
|
93873cd845 | ||
|
|
3def37d28c | ||
|
|
d7e7b8c873 | ||
|
|
b401f7bb34 | ||
|
|
952d5d89e7 | ||
|
|
15d9c9f1bd | ||
|
|
b5e22c983d | ||
|
|
4d3b68d287 | ||
|
|
2607cec626 | ||
|
|
ec18af1ec0 | ||
|
|
ee62aab0e7 | ||
|
|
3cb55b320a | ||
|
|
d2e26bf8cf | ||
|
|
95fb4ec859 | ||
|
|
03f0ec4aec | ||
|
|
9dc447fa85 | ||
|
|
0286304c39 | ||
|
|
d88c4ade78 | ||
|
|
e3b8143677 | ||
|
|
09384b317e | ||
|
|
2172ea4041 | ||
|
|
7e3aee7147 | ||
|
|
4f187390a0 | ||
|
|
5b3053d3bb | ||
|
|
dae1e03e73 | ||
|
|
a32c79984d | ||
|
|
8d0596a32e | ||
|
|
91b1229a29 |
@@ -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);
|
||||||
|
|||||||
11
src/alerts/logistration-alert/messages.js
Normal file
11
src/alerts/logistration-alert/messages.js
Normal 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;
|
||||||
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export {
|
|||||||
fetchProgressTab,
|
fetchProgressTab,
|
||||||
resetDeadlines,
|
resetDeadlines,
|
||||||
saveCourseGoal,
|
saveCourseGoal,
|
||||||
|
saveWeeklyCourseGoal,
|
||||||
} from './thunks';
|
} from './thunks';
|
||||||
|
|
||||||
export { reducer } from './slice';
|
export { reducer } from './slice';
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 you’re not quite at your goal, we’ll 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;
|
||||||
|
|||||||
51
src/course-home/outline-tab/widgets/FlagButton.jsx
Normal file
51
src/course-home/outline-tab/widgets/FlagButton.jsx
Normal 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;
|
||||||
@@ -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);
|
||||||
173
src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx
Normal file
173
src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx
Normal 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);
|
||||||
18
src/course-home/outline-tab/widgets/flag_gray.svg
Normal file
18
src/course-home/outline-tab/widgets/flag_gray.svg
Normal 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 |
3
src/course-home/outline-tab/widgets/flag_outline.svg
Normal file
3
src/course-home/outline-tab/widgets/flag_outline.svg
Normal 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 |
Reference in New Issue
Block a user