feat: Create three day streak celebration (#354)
Show learners a celebratory modal if they visit the learning mfe for 3 days in a row. Call edx-platform API to determine if they should see the celebration. AA-304
This commit is contained in:
committed by
GitHub
parent
26a7b3b0de
commit
2525805aac
4
src/shared/README.md
Normal file
4
src/shared/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## Shared module (src/shared)
|
||||
|
||||
This module is a place for shared code that is specific to the frontend-app-learning micro-frontend.
|
||||
If the code is more generic and could be extracted into a reusable code repository like Paragon in the future, then it belongs in the src/generic module.
|
||||
118
src/shared/streak-celebration/StreakCelebrationModal.jsx
Normal file
118
src/shared/streak-celebration/StreakCelebrationModal.jsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Lightbulb } from '@edx/paragon/icons';
|
||||
import { Icon, Modal } from '@edx/paragon';
|
||||
import { layoutGenerator } from 'react-break';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import StreakMobileImage from './assets/Streak_mobile.png';
|
||||
import StreakDesktopImage from './assets/Streak_desktop.png';
|
||||
import messages from './messages';
|
||||
import { recordModalClosing, recordStreakCelebration } from './utils';
|
||||
|
||||
function getRandomFactoid(intl, streakLength) {
|
||||
const boldedSectionA = intl.formatMessage(messages.streakFactoidABoldedSection);
|
||||
const boldedSectionB = intl.formatMessage(messages.streakFactoidBBoldedSection);
|
||||
const factoids = [
|
||||
(<FormattedMessage
|
||||
id="learning.streakcelebration.factoida"
|
||||
defaultMessage="Users who learn {streak_length} days in a row {bolded_section} than those who don’t."
|
||||
values={{
|
||||
bolded_section: (<b>{boldedSectionA}</b>),
|
||||
streak_length: (streakLength),
|
||||
}}
|
||||
/>),
|
||||
(<FormattedMessage
|
||||
id="learning.streakcelebration.factoidb"
|
||||
defaultMessage="Users who learn {streak_length} days in a row {bolded_section} vs. those who don’t."
|
||||
values={{
|
||||
bolded_section: (<b>{boldedSectionB}</b>),
|
||||
streak_length: (streakLength),
|
||||
}}
|
||||
/>),
|
||||
];
|
||||
return factoids[Math.floor(Math.random() * (factoids.length))];
|
||||
}
|
||||
|
||||
function StreakModal({
|
||||
courseId, metadataModel, streakLengthToCelebrate, intl, open, ...rest
|
||||
}) {
|
||||
const { org, celebrations } = useModel(metadataModel, courseId);
|
||||
const factoid = getRandomFactoid(intl, streakLengthToCelebrate);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [randomFactoid, setRandomFactoid] = useState(factoid); // Don't change factoid on re-render
|
||||
|
||||
const layout = layoutGenerator({
|
||||
mobile: 0,
|
||||
desktop: 575,
|
||||
});
|
||||
|
||||
const OnMobile = layout.is('mobile');
|
||||
const OnDesktop = layout.isAtLeast('desktop');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
recordStreakCelebration(org, courseId);
|
||||
}
|
||||
}, [open, org, courseId]);
|
||||
|
||||
function CloseText() {
|
||||
return (
|
||||
<span>
|
||||
{intl.formatMessage(messages.streakButton)}
|
||||
<span className="sr-only">. {intl.formatMessage(messages.streakButtonSrOnly)}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
dialogClassName="streak-modal modal-dialog-centered"
|
||||
body={(
|
||||
<>
|
||||
<p>{intl.formatMessage(messages.streakBody)}</p>
|
||||
<p className="modal-image">
|
||||
<OnMobile>
|
||||
<img src={StreakMobileImage} alt="" className="img-fluid" />
|
||||
</OnMobile>
|
||||
<OnDesktop>
|
||||
<img src={StreakDesktopImage} alt="" className="img-fluid" />
|
||||
</OnDesktop>
|
||||
</p>
|
||||
<div className="row mt-3 mx-3 py-3 bg-light-300">
|
||||
<Icon className="col-small ml-3" src={Lightbulb} />
|
||||
<div className="col-11 factoid-wrapper">
|
||||
{randomFactoid}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
closeText={<CloseText />}
|
||||
onClose={() => {
|
||||
recordModalClosing(metadataModel, celebrations, org, courseId, dispatch);
|
||||
}}
|
||||
open={open}
|
||||
title={`${streakLengthToCelebrate} ${intl.formatMessage(messages.streakHeader)}`}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
StreakModal.defaultProps = {
|
||||
open: false,
|
||||
};
|
||||
|
||||
StreakModal.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
metadataModel: PropTypes.string.isRequired,
|
||||
streakLengthToCelebrate: PropTypes.number.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
open: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(StreakModal);
|
||||
67
src/shared/streak-celebration/StreakCelebrationModal.scss
Normal file
67
src/shared/streak-celebration/StreakCelebrationModal.scss
Normal file
@@ -0,0 +1,67 @@
|
||||
.streak-modal {
|
||||
text-align: center;
|
||||
|
||||
.modal-header {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0; // override default hr line
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
// This lets us center the modal title at full width, without taking button width into account
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
button::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding-top: .5rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 0; // override default hr line
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
@extend .btn-primary;
|
||||
font-size: 1.2rem;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
margin-top: 1.875rem;
|
||||
margin-bottom: 1.875rem;
|
||||
}
|
||||
|
||||
.factoid-wrapper {
|
||||
font-size: .875rem;
|
||||
text-align: left;
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (min-width: 570px) {
|
||||
.streak-modal {
|
||||
width: 33.25rem;
|
||||
max-width: 33.25rem;
|
||||
|
||||
.calendar {
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.factoid-wrapper {
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Factory } from 'rosie';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { initializeTestStore, render, screen } from '../../setupTest';
|
||||
import StreakModal from './StreakCelebrationModal';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
describe('Loaded Tab Page', () => {
|
||||
const mockData = { metadataModel: 'coursewareMeta' };
|
||||
|
||||
beforeAll(async () => {
|
||||
mockData.open = true;
|
||||
mockData.streakLengthToCelebrate = 3;
|
||||
});
|
||||
|
||||
it('shows streak celebration modal', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata', { celebrations: { shouldCelebrateStreak: true } });
|
||||
mockData.courseId = courseMetadata.id;
|
||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||
render(<StreakModal {...mockData} courseId={courseMetadata.id} />, { store: testStore });
|
||||
|
||||
await screen.findByText('3 day streak');
|
||||
await screen.findByText('Keep it up, you’re on a roll!');
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.celebration.streak.opened', {
|
||||
org_key: courseMetadata.org,
|
||||
courserun_key: mockData.courseId,
|
||||
is_staff: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
BIN
src/shared/streak-celebration/assets/Streak_desktop.png
Normal file
BIN
src/shared/streak-celebration/assets/Streak_desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/shared/streak-celebration/assets/Streak_mobile.png
Normal file
BIN
src/shared/streak-celebration/assets/Streak_mobile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
1
src/shared/streak-celebration/index.js
Normal file
1
src/shared/streak-celebration/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './StreakCelebrationModal';
|
||||
34
src/shared/streak-celebration/messages.js
Normal file
34
src/shared/streak-celebration/messages.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
streakHeader: {
|
||||
id: 'learning.streakCelebration.header',
|
||||
defaultMessage: 'day streak',
|
||||
description: 'Will come after a number. For example, 3 day streak',
|
||||
},
|
||||
streakBody: {
|
||||
id: 'learning.streakCelebration.body',
|
||||
defaultMessage: 'Keep it up, you’re on a roll!',
|
||||
},
|
||||
streakButton: {
|
||||
id: 'learning.streakCelebration.button',
|
||||
defaultMessage: 'Keep it up',
|
||||
},
|
||||
streakButtonSrOnly: {
|
||||
id: 'learning.streakCelebration.buttonSrOnly',
|
||||
defaultMessage: 'Close modal and continue',
|
||||
description: 'Screenreader label for streakButton text',
|
||||
},
|
||||
streakFactoidABoldedSection: {
|
||||
id: 'learning.streakCelebration.factoidABoldedSection',
|
||||
defaultMessage: 'are 20x more likely to pass their course',
|
||||
description: 'This bolded section is in the following sentence: Users who learn 3 days in a row {bolded_section} than those who don\'t.',
|
||||
},
|
||||
streakFactoidBBoldedSection: {
|
||||
id: 'learning.streakCelebration.factoidBBoldedSection',
|
||||
defaultMessage: 'complete 5x as much course content on average',
|
||||
description: 'This bolded section is in the following sentence: Users who learn 3 days in a row {bolded_section} vs. those who don\'t.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
27
src/shared/streak-celebration/utils.jsx
Normal file
27
src/shared/streak-celebration/utils.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { updateModel } from '../../generic/model-store';
|
||||
|
||||
function recordStreakCelebration(org, courseId) {
|
||||
// Tell our analytics
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
sendTrackEvent('edx.ui.lms.celebration.streak.opened', {
|
||||
org_key: org,
|
||||
courserun_key: courseId,
|
||||
is_staff: administrator,
|
||||
});
|
||||
}
|
||||
|
||||
function recordModalClosing(metadataModel, celebrations, org, courseId, dispatch) {
|
||||
// Ensure we only celebrate each streak once
|
||||
dispatch(updateModel({
|
||||
modelType: metadataModel,
|
||||
model: {
|
||||
id: courseId,
|
||||
celebrations: { ...celebrations, shouldCelebrateStreak: false },
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
export { recordStreakCelebration, recordModalClosing };
|
||||
Reference in New Issue
Block a user