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:
Chris Deery
2021-09-17 11:40:12 -04:00
parent 03f0ec4aec
commit 95fb4ec859
9 changed files with 271 additions and 0 deletions

View File

@@ -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 });

View File

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

View File

@@ -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

View File

@@ -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}
/>

View File

@@ -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',

View 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;

View 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);

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