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
This commit is contained in:
@@ -391,6 +391,15 @@ export async function postCourseGoals(courseId, 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) {
|
||||
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`);
|
||||
await getAuthenticatedHttpClient().post(url.href, { course_id: courseId });
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
fetchProgressTab,
|
||||
resetDeadlines,
|
||||
saveCourseGoal,
|
||||
saveWeeklyCourseGoal,
|
||||
} from './thunks';
|
||||
|
||||
export { reducer } from './slice';
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getProgressTabData,
|
||||
postCourseDeadlines,
|
||||
postCourseGoals,
|
||||
postWeeklyCourseGoals,
|
||||
postDismissWelcomeMessage,
|
||||
postRequestCert,
|
||||
} from './api';
|
||||
@@ -113,6 +114,10 @@ export async function saveCourseGoal(courseId, goalKey) {
|
||||
return postCourseGoals(courseId, goalKey);
|
||||
}
|
||||
|
||||
export async function saveWeeklyCourseGoal(courseId, daysPerWeek, subscribedToReminders) {
|
||||
return postWeeklyCourseGoals(courseId, daysPerWeek, subscribedToReminders);
|
||||
}
|
||||
|
||||
export function processEvent(eventData, getTabData) {
|
||||
return async (dispatch) => {
|
||||
// Pulling this out early so the data doesn't get camelCased and is easier
|
||||
|
||||
@@ -32,6 +32,7 @@ import AccountActivationAlert from '../../alerts/logistration-alert/AccountActiv
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
import { initHomeMMP2P, MMP2PFlyover } from '../../experiments/mm-p2p';
|
||||
import WeeklyLearningGoal from './widgets/WeeklyLearningGoal';
|
||||
|
||||
function OutlineTab({ intl }) {
|
||||
const {
|
||||
@@ -229,6 +230,13 @@ function OutlineTab({ intl }) {
|
||||
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
|
||||
/>
|
||||
)}
|
||||
{numberOfDaysGoalsEnabled && (
|
||||
<WeeklyLearningGoal
|
||||
selectedGoal={selectedGoal}
|
||||
daysPerWeek={selectedGoal && 'daysPerWeek' in selectedGoal ? selectedGoal.daysPerWeek : 0}
|
||||
courseId={courseId}
|
||||
/>
|
||||
)}
|
||||
<CourseTools
|
||||
courseId={courseId}
|
||||
/>
|
||||
|
||||
@@ -82,6 +82,14 @@ 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.',
|
||||
},
|
||||
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',
|
||||
@@ -120,6 +128,38 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Welcome to',
|
||||
description: 'This precedes the title of the course',
|
||||
},
|
||||
goalButtonTitleCasual: {
|
||||
id: 'learning.outline.goalButtonTitleCasual',
|
||||
defaultMessage: 'Casual',
|
||||
},
|
||||
goalButtonTextCasual: {
|
||||
id: 'learning.outline.goalButtonTitleCasual',
|
||||
defaultMessage: '1 day a week',
|
||||
},
|
||||
goalButtonTitleRegular: {
|
||||
id: 'learning.outline.goalButtonTitleRegular',
|
||||
defaultMessage: 'Regular',
|
||||
},
|
||||
goalButtonTextRegular: {
|
||||
id: 'learning.outline.goalButtonTitleRegular',
|
||||
defaultMessage: '3 days a week',
|
||||
},
|
||||
goalButtonTitleIntense: {
|
||||
id: 'learning.outline.goalButtonTitleIntense',
|
||||
defaultMessage: 'Intense',
|
||||
},
|
||||
goalButtonTextIntense: {
|
||||
id: 'learning.outline.goalButtonTitleIntense',
|
||||
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: {
|
||||
id: 'learning.proctoringPanel.header',
|
||||
defaultMessage: 'This course contains proctored exams',
|
||||
|
||||
53
src/course-home/outline-tab/widgets/FlagButton.jsx
Normal file
53
src/course-home/outline-tab/widgets/FlagButton.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function FlagButton({
|
||||
icon,
|
||||
title,
|
||||
text,
|
||||
isEnabled,
|
||||
handleSelect,
|
||||
}) {
|
||||
const baseOutlineStyle = 'col flex-grow-1 p-3 border border-light rounded bg-white';
|
||||
const selectedOutlineStyle = 'col flex-grow-1 p-3 border border-dark rounded bg-white';
|
||||
const [isHighlight, setHighlight] = useState(false);
|
||||
|
||||
function getOutlineStyle() {
|
||||
return isEnabled || isHighlight ? selectedOutlineStyle : baseOutlineStyle;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={getOutlineStyle()}
|
||||
onMouseEnter={() => setHighlight(true)}
|
||||
onMouseLeave={() => setHighlight(false)}
|
||||
onClick={() => handleSelect()}
|
||||
>
|
||||
<div className=" justify-content-center">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="text-center small">
|
||||
{title}
|
||||
</div>
|
||||
<div className="text-center micro">
|
||||
{text}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
FlagButton.propTypes = {
|
||||
icon: PropTypes.node.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
text: PropTypes.string,
|
||||
isEnabled: PropTypes.bool,
|
||||
handleSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
FlagButton.defaultProps = {
|
||||
isEnabled: false,
|
||||
text: '',
|
||||
};
|
||||
|
||||
export default FlagButton;
|
||||
134
src/course-home/outline-tab/widgets/WeeklyLearningGoal.jsx
Normal file
134
src/course-home/outline-tab/widgets/WeeklyLearningGoal.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
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 WeeklyLearningGoal({
|
||||
selectedGoal,
|
||||
courseId,
|
||||
intl,
|
||||
}) {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const [daysPerWeekGoal, setDaysPerWeekGoal] = useState('daysPerWeek' in selectedGoal ? selectedGoal.daysPerWeek : 0);
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const [isGetReminderChecked, setGetReminderChecked] = useState('subscribedToReminders' in selectedGoal ? selectedGoal.subscribedToReminders : false);
|
||||
const LevelToDays = {
|
||||
CASUAL: 3,
|
||||
REGULAR: 4,
|
||||
INTENSE: 5,
|
||||
};
|
||||
Object.freeze(LevelToDays);
|
||||
|
||||
function handleSelect(days) {
|
||||
setDaysPerWeekGoal(days);
|
||||
saveWeeklyCourseGoal(courseId, days, isGetReminderChecked).then((response) => {
|
||||
const { data } = response;
|
||||
const {
|
||||
header,
|
||||
message,
|
||||
} = data;
|
||||
// TODO: add Toast?, remove console.log
|
||||
console.log(header, ':', message);
|
||||
});
|
||||
}
|
||||
|
||||
function handleSubscribeToReminders(event) {
|
||||
const isGetReminders = event.target.checked;
|
||||
setGetReminderChecked(isGetReminders);
|
||||
saveWeeklyCourseGoal(courseId, daysPerWeekGoal, isGetReminders).then((response) => {
|
||||
const { data } = response;
|
||||
const {
|
||||
header,
|
||||
message,
|
||||
} = data;
|
||||
// TODO: add Toast?, remove console.log
|
||||
console.log(header, ':', message);
|
||||
});
|
||||
}
|
||||
const buttonRowStyle = 'row w-100 m-0 p-0 justify-content-around'; // 'row w-100 m-0 flex-grow-1 p-0 justify content-end';
|
||||
const flagButtonStyle = 'col-auto col-md-12 col-xl-auto m-0 p-0 pb-md-3 pb-xl-0'; // 'col-auto flex-grow-1 p-0';
|
||||
|
||||
return (
|
||||
<div className="row w-100 m-0 p-0">
|
||||
<Card className="mb-3" data-testid="course-goal-card">
|
||||
<Card.Body>
|
||||
<Card.Title>
|
||||
<div className="h4 m-0">{intl.formatMessage(messages.setWeeklyGoal)}</div>
|
||||
</Card.Title>
|
||||
<Card.Text>
|
||||
{intl.formatMessage(messages.setWeeklyGoalDetail)}
|
||||
</Card.Text>
|
||||
<div className={buttonRowStyle}>
|
||||
<div className={flagButtonStyle}>
|
||||
<FlagButton
|
||||
icon={<FlagCasualIcon />}
|
||||
title={intl.formatMessage(messages.goalButtonTitleCasual)}
|
||||
text={intl.formatMessage(messages.goalButtonTextCasual)}
|
||||
isEnabled={daysPerWeekGoal === LevelToDays.CASUAL}
|
||||
handleSelect={() => { handleSelect(LevelToDays.CASUAL); }}
|
||||
/>
|
||||
</div>
|
||||
<div className={flagButtonStyle}>
|
||||
<FlagButton
|
||||
icon={<FlagRegularIcon />}
|
||||
title={intl.formatMessage(messages.goalButtonTitleRegular)}
|
||||
text={intl.formatMessage(messages.goalButtonTextRegular)}
|
||||
isEnabled={daysPerWeekGoal === LevelToDays.REGULAR}
|
||||
handleSelect={() => { handleSelect(LevelToDays.REGULAR); }}
|
||||
/>
|
||||
</div>
|
||||
<div className={flagButtonStyle}>
|
||||
<FlagButton
|
||||
icon={<FlagIntenseIcon />}
|
||||
title={intl.formatMessage(messages.goalButtonTitleIntense)}
|
||||
text={intl.formatMessage(messages.goalButtonTextIntense)}
|
||||
isEnabled={daysPerWeekGoal === LevelToDays.INTENSE}
|
||||
handleSelect={() => { handleSelect(LevelToDays.INTENSE); }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row p-3">
|
||||
<Form.Switch
|
||||
checked={isGetReminderChecked}
|
||||
onChange={(event) => handleSubscribeToReminders(event)}
|
||||
>
|
||||
{intl.formatMessage(messages.setGoalReminder)}
|
||||
</Form.Switch>
|
||||
</div>
|
||||
</Card.Body>
|
||||
{/* This is supposed to fill with gray in the bottom of the card */}
|
||||
{isGetReminderChecked && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
WeeklyLearningGoal.propTypes = {
|
||||
selectedGoal: PropTypes.shape({}).isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(WeeklyLearningGoal);
|
||||
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