AA-137: Add first-section celebration (#78)
When a learner completes their first section in a course, throw up a modal that celebrates that fact and encourages them to share progress.
This commit is contained in:
1
.env
1
.env
@@ -14,5 +14,6 @@ ORDER_HISTORY_URL=null
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME=null
|
||||
TWITTER_URL=null
|
||||
STUDIO_BASE_URL=
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
|
||||
@@ -14,5 +14,6 @@ PORT=2000
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
TWITTER_URL='https://twitter.com/edXOnline'
|
||||
STUDIO_BASE_URL='http://localhost:18010'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
|
||||
@@ -14,5 +14,6 @@ PORT=2000
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
TWITTER_URL='https://twitter.com/edXOnline'
|
||||
STUDIO_BASE_URL='http://localhost:18010'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
|
||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -5196,6 +5196,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"breakjs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/breakjs/-/breakjs-1.0.0.tgz",
|
||||
"integrity": "sha1-7INToGhi60OWLergkHLuZqTNhFk="
|
||||
},
|
||||
"brorand": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
|
||||
@@ -12330,6 +12335,24 @@
|
||||
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
|
||||
"dev": true
|
||||
},
|
||||
"jsonp": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz",
|
||||
"integrity": "sha1-pltPoPEL2nGaBUQep7lMVfPhW64=",
|
||||
"requires": {
|
||||
"debug": "^2.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
@@ -15565,6 +15588,16 @@
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"react-break": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/react-break/-/react-break-1.3.2.tgz",
|
||||
"integrity": "sha512-gm5paFth+ac+Ag35l1X0/V/XmdQn+Y+YggNInqaVXGHBrsODCBu8aXQpOsilYl+MfY6TL3eCJpkwuX1FVhDcpg==",
|
||||
"requires": {
|
||||
"babel-runtime": "^6.10.0",
|
||||
"breakjs": "^1.0.0",
|
||||
"prop-types": "^15.6.0"
|
||||
}
|
||||
},
|
||||
"react-dev-utils": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.0.3.tgz",
|
||||
@@ -15806,6 +15839,15 @@
|
||||
"tiny-warning": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-share": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-share/-/react-share-4.1.0.tgz",
|
||||
"integrity": "sha512-4/XqVBC+hreniU08zkzAkOGC3FPdJhcLzt1QCdybsZPd5ieUS++iChEEptvNoksiJ5NxbI2VRoDY9ovLAGl7GQ==",
|
||||
"requires": {
|
||||
"classnames": "^2.2.5",
|
||||
"jsonp": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz",
|
||||
|
||||
@@ -48,10 +48,12 @@
|
||||
"core-js": "^3.6.2",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.12.0",
|
||||
"react-break": "^1.3.2",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-share": "^4.1.0",
|
||||
"redux": "^4.0.5",
|
||||
"regenerator-runtime": "^0.13.3"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>Course | edX</title>
|
||||
<title>Course | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TabPage } from '../tab-page';
|
||||
|
||||
import Course from './course';
|
||||
import { sequenceIdsSelector, firstSequenceIdSelector } from './data/selectors';
|
||||
import { handleNextSectionCelebration } from './course/celebration';
|
||||
|
||||
function useUnitNavigationHandler(courseId, sequenceId, unitId) {
|
||||
const dispatch = useDispatch();
|
||||
@@ -52,6 +53,8 @@ function useNextSequence(sequenceId) {
|
||||
|
||||
|
||||
function useNextSequenceHandler(courseId, sequenceId) {
|
||||
const course = useModel('courses', courseId);
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const nextSequence = useNextSequence(sequenceId);
|
||||
const courseStatus = useSelector(state => state.courseware.courseStatus);
|
||||
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
||||
@@ -64,6 +67,11 @@ function useNextSequenceHandler(courseId, sequenceId) {
|
||||
// Some sequences have no units. This will show a blank page with prev/next buttons.
|
||||
history.push(`/course/${courseId}/${nextSequence.id}`);
|
||||
}
|
||||
|
||||
const celebrateFirstSection = course && course.celebrations && course.celebrations.firstSection;
|
||||
if (celebrateFirstSection && sequence.sectionId !== nextSequence.sectionId) {
|
||||
handleNextSectionCelebration(sequenceId, nextSequence.id);
|
||||
}
|
||||
}
|
||||
}, [courseStatus, sequenceStatus, sequenceId]);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useOfferAlert } from '../../alerts/offer-alert';
|
||||
|
||||
import Sequence from './sequence';
|
||||
|
||||
import { CelebrationModal, shouldCelebrateOnSectionLoad } from './celebration';
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
import CourseSock from './course-sock';
|
||||
import ContentTools from './content-tools';
|
||||
@@ -39,9 +40,13 @@ function Course({
|
||||
|
||||
const {
|
||||
canShowUpgradeSock,
|
||||
celebrations,
|
||||
verifiedMode,
|
||||
} = course;
|
||||
|
||||
const celebrateFirstSection = celebrations && celebrations.firstSection;
|
||||
const celebrationOpen = shouldCelebrateOnSectionLoad(courseId, sequenceId, celebrateFirstSection);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertList
|
||||
@@ -73,6 +78,12 @@ function Course({
|
||||
nextSequenceHandler={nextSequenceHandler}
|
||||
previousSequenceHandler={previousSequenceHandler}
|
||||
/>
|
||||
{celebrationOpen && (
|
||||
<CelebrationModal
|
||||
courseId={courseId}
|
||||
open
|
||||
/>
|
||||
)}
|
||||
{canShowUpgradeSock && verifiedMode && <CourseSock verifiedMode={verifiedMode} />}
|
||||
<ContentTools course={course} />
|
||||
</>
|
||||
|
||||
68
src/courseware/course/celebration/CelebrationModal.jsx
Normal file
68
src/courseware/course/celebration/CelebrationModal.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Modal } from '@edx/paragon';
|
||||
import { layoutGenerator } from 'react-break';
|
||||
|
||||
import ClapsMobile from './assets/claps_280x201.gif';
|
||||
import ClapsTablet from './assets/claps_456x328.gif';
|
||||
import messages from './messages';
|
||||
import SocialIcons from './SocialIcons';
|
||||
import { recordFirstSectionCelebration } from './utils';
|
||||
|
||||
import './CelebrationModal.scss';
|
||||
|
||||
function CelebrationModal({
|
||||
courseId, intl, open, ...rest
|
||||
}) {
|
||||
const layout = layoutGenerator({
|
||||
mobile: 0,
|
||||
tablet: 400,
|
||||
});
|
||||
|
||||
const OnMobile = layout.is('mobile');
|
||||
const OnAtLeastTablet = layout.isAtLeast('tablet');
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
recordFirstSectionCelebration(courseId);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
body={(
|
||||
<>
|
||||
<p>{intl.formatMessage(messages.completed)}</p>
|
||||
<OnMobile>
|
||||
<img src={ClapsMobile} alt="" className="img-fluid" />
|
||||
</OnMobile>
|
||||
<OnAtLeastTablet>
|
||||
<img src={ClapsTablet} alt="" className="img-fluid w-100" />
|
||||
</OnAtLeastTablet>
|
||||
<p className="mt-3">
|
||||
<strong>{intl.formatMessage(messages.earned)}</strong> {intl.formatMessage(messages.share)}
|
||||
</p>
|
||||
<SocialIcons courseId={courseId} />
|
||||
</>
|
||||
)}
|
||||
closeText={intl.formatMessage(messages.forward)}
|
||||
onClose={() => {}} // Don't do anything special, just having the modal close is enough (this is a required prop)
|
||||
open={open}
|
||||
title={intl.formatMessage(messages.congrats)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
CelebrationModal.defaultProps = {
|
||||
open: false,
|
||||
};
|
||||
|
||||
CelebrationModal.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
open: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(CelebrationModal);
|
||||
35
src/courseware/course/celebration/CelebrationModal.scss
Normal file
35
src/courseware/course/celebration/CelebrationModal.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
@import '~@edx/paragon/scss/edx/theme.scss';
|
||||
|
||||
.modal {
|
||||
text-align: center;
|
||||
|
||||
.modal-header {
|
||||
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: "⨉";
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/courseware/course/celebration/SocialIcons.jsx
Normal file
71
src/courseware/course/celebration/SocialIcons.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FacebookIcon,
|
||||
FacebookShareButton,
|
||||
TwitterIcon,
|
||||
TwitterShareButton,
|
||||
} from 'react-share';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import { useModel } from '../../../model-store';
|
||||
|
||||
function SocialIcons({ courseId, intl }) {
|
||||
const {
|
||||
marketingUrl,
|
||||
title,
|
||||
} = useModel('courses', courseId);
|
||||
|
||||
if (!marketingUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const twitterUrl = getConfig().TWITTER_URL;
|
||||
const twitterAccount = twitterUrl && twitterUrl.substring(twitterUrl.lastIndexOf('/') + 1);
|
||||
|
||||
const logClick = (service) => {
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
sendTrackEvent('edx.ui.lms.celebration.social_share.clicked', {
|
||||
course_id: courseId,
|
||||
is_staff: administrator,
|
||||
service,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="social-icons">
|
||||
{ twitterAccount && (
|
||||
<TwitterShareButton
|
||||
beforeOnClick={() => logClick('twitter')}
|
||||
hashtags={['mooc']}
|
||||
title={intl.formatMessage(messages.social, { platform: `@${twitterAccount}`, title })}
|
||||
url={marketingUrl}
|
||||
>
|
||||
<TwitterIcon round size={32} />
|
||||
<span className="sr-only">{intl.formatMessage(messages.shareService, { service: 'Twitter' })}</span>
|
||||
</TwitterShareButton>
|
||||
)}
|
||||
<FacebookShareButton
|
||||
beforeOnClick={() => logClick('facebook')}
|
||||
className="ml-2"
|
||||
quote={intl.formatMessage(messages.social, { platform: getConfig().SITE_NAME, title })}
|
||||
url={marketingUrl}
|
||||
>
|
||||
<FacebookIcon round size={32} />
|
||||
<span className="sr-only">{intl.formatMessage(messages.shareService, { service: 'Facebook' })}</span>
|
||||
</FacebookShareButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SocialIcons.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SocialIcons);
|
||||
BIN
src/courseware/course/celebration/assets/claps_280x201.gif
Normal file
BIN
src/courseware/course/celebration/assets/claps_280x201.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
src/courseware/course/celebration/assets/claps_456x328.gif
Normal file
BIN
src/courseware/course/celebration/assets/claps_456x328.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
2
src/courseware/course/celebration/index.js
Normal file
2
src/courseware/course/celebration/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as CelebrationModal } from './CelebrationModal';
|
||||
export { handleNextSectionCelebration, shouldCelebrateOnSectionLoad } from './utils';
|
||||
36
src/courseware/course/celebration/messages.js
Normal file
36
src/courseware/course/celebration/messages.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
completed: {
|
||||
id: 'learning.celebration.completed',
|
||||
defaultMessage: 'You just completed the first section of your course.',
|
||||
},
|
||||
congrats: {
|
||||
id: 'learning.celebration.congrats',
|
||||
defaultMessage: 'Congratulations!',
|
||||
},
|
||||
earned: {
|
||||
id: 'learning.celebration.earned',
|
||||
defaultMessage: 'You earned it!',
|
||||
},
|
||||
forward: {
|
||||
id: 'learning.celebration.forward',
|
||||
defaultMessage: 'Keep going',
|
||||
description: 'Button to close celebration dialog and get back to course',
|
||||
},
|
||||
share: {
|
||||
id: 'learning.celebration.share',
|
||||
defaultMessage: 'Take a moment to celebrate and share your progress.',
|
||||
},
|
||||
shareService: {
|
||||
id: 'learning.celebration.share.service',
|
||||
defaultMessage: 'Share your progress on {service}.',
|
||||
},
|
||||
social: {
|
||||
id: 'learning.celebration.social',
|
||||
defaultMessage: 'I’m on my way to completing {title} online with {platform}. What are you spending your time learning?',
|
||||
description: 'Shown when sharing course progress on a social network',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
51
src/courseware/course/celebration/utils.jsx
Normal file
51
src/courseware/course/celebration/utils.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { setFirstSectionCelebrationComplete } from '../../../data/api';
|
||||
import { clearLocalStorage, getLocalStorage, setLocalStorage } from '../../../data/localStorage';
|
||||
|
||||
const CELEBRATION_LOCAL_STORAGE_KEY = 'CelebrationModal.showOnSectionLoad';
|
||||
|
||||
// Records clicks through the end of a section, so that we can know whether we should celebrate when we finish loading
|
||||
function handleNextSectionCelebration(sequenceId, nextSequenceId) {
|
||||
setLocalStorage(CELEBRATION_LOCAL_STORAGE_KEY, {
|
||||
prevSequenceId: sequenceId,
|
||||
nextSequenceId,
|
||||
});
|
||||
}
|
||||
|
||||
function recordFirstSectionCelebration(courseId) {
|
||||
// Tell the LMS
|
||||
setFirstSectionCelebrationComplete(courseId);
|
||||
|
||||
// Tell our analytics
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
sendTrackEvent('edx.ui.lms.celebration.first_section.opened', {
|
||||
course_id: courseId,
|
||||
is_staff: administrator,
|
||||
});
|
||||
}
|
||||
|
||||
// Looks at local storage to see whether we just came from the end of a section.
|
||||
// Note! This does have side effects (will clear some local storage and may start an api call).
|
||||
function shouldCelebrateOnSectionLoad(courseId, sequenceId, celebrateFirstSection) {
|
||||
const celebrationIds = getLocalStorage(CELEBRATION_LOCAL_STORAGE_KEY);
|
||||
if (!celebrationIds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
prevSequenceId,
|
||||
nextSequenceId,
|
||||
} = celebrationIds;
|
||||
const shouldCelebrate = sequenceId === nextSequenceId && celebrateFirstSection;
|
||||
|
||||
if (sequenceId !== prevSequenceId && sequenceId !== nextSequenceId) {
|
||||
// Don't clear until we move off of current/prev sequence
|
||||
clearLocalStorage(CELEBRATION_LOCAL_STORAGE_KEY);
|
||||
}
|
||||
|
||||
return shouldCelebrate;
|
||||
}
|
||||
|
||||
export { handleNextSectionCelebration, recordFirstSectionCelebration, shouldCelebrateOnSectionLoad };
|
||||
@@ -37,6 +37,8 @@ function normalizeMetadata(metadata) {
|
||||
tabs: normalizeTabUrls(metadata.id, camelCaseObject(metadata.tabs)),
|
||||
showCalculator: metadata.show_calculator,
|
||||
notes: camelCaseObject(metadata.notes),
|
||||
marketingUrl: metadata.marketing_url,
|
||||
celebrations: camelCaseObject(metadata.celebrations),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -227,6 +229,14 @@ export async function getResumeBlock(courseId) {
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
// Does not block on answer
|
||||
export function setFirstSectionCelebrationComplete(courseId) {
|
||||
const url = new URL(`${getConfig().LMS_BASE_URL}/api/courseware/celebration/${courseId}`);
|
||||
getAuthenticatedHttpClient().post(url.href, {
|
||||
first_section: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateCourseDeadlines(courseId) {
|
||||
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`);
|
||||
await getAuthenticatedHttpClient().post(url.href, { course_key: courseId });
|
||||
|
||||
51
src/data/localStorage.js
Normal file
51
src/data/localStorage.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// This file holds some convenience methods for dealing with localStorage.
|
||||
//
|
||||
// NOTE: These storage keys are not namespaced. That means that it's shared for the current fully
|
||||
// qualified domain. Namespacing could be added, but we'll cross that bridge when we need it.
|
||||
|
||||
function getLocalStorage(key) {
|
||||
try {
|
||||
if (global.localStorage) {
|
||||
const rawItem = global.localStorage.getItem(key);
|
||||
if (rawItem) {
|
||||
return JSON.parse(rawItem);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If this fails for some reason, just return null.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setLocalStorage(key, value) {
|
||||
try {
|
||||
if (global.localStorage) {
|
||||
global.localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
} catch (e) {
|
||||
// If this fails, just bail.
|
||||
}
|
||||
}
|
||||
|
||||
function clearLocalStorage(key) {
|
||||
try {
|
||||
if (global.localStorage) {
|
||||
global.localStorage.removeItem(key);
|
||||
}
|
||||
} catch (e) {
|
||||
// If this fails, just bail.
|
||||
}
|
||||
}
|
||||
|
||||
function popLocalStorage(key) {
|
||||
const value = getLocalStorage(key);
|
||||
clearLocalStorage(key);
|
||||
return value;
|
||||
}
|
||||
|
||||
export {
|
||||
clearLocalStorage,
|
||||
getLocalStorage,
|
||||
popLocalStorage,
|
||||
setLocalStorage,
|
||||
};
|
||||
@@ -69,6 +69,7 @@ initialize({
|
||||
mergeConfig({
|
||||
INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null,
|
||||
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null,
|
||||
TWITTER_URL: process.env.TWITTER_URL || null,
|
||||
}, 'LearnerAppConfig');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import UserMessagesContext from './UserMessagesContext';
|
||||
import { getLocalStorage, popLocalStorage, setLocalStorage } from '../data/localStorage';
|
||||
|
||||
export const ALERT_TYPES = {
|
||||
ERROR: 'error',
|
||||
@@ -10,50 +11,19 @@ export const ALERT_TYPES = {
|
||||
INFO: 'info',
|
||||
};
|
||||
|
||||
// NOTE: This storage key is not namespaced. That means that it's shared for the current fully
|
||||
// qualified domain. Namespacing could be added by adding an optional prop to UserMessagesProvider
|
||||
// to set a namespace, but we'll cross that bridge when we need it.
|
||||
const FLASH_MESSAGES_LOCAL_STORAGE_KEY = 'UserMessagesProvider.flashMessages';
|
||||
|
||||
function getFlashMessages() {
|
||||
let flashMessages = [];
|
||||
try {
|
||||
if (global.localStorage) {
|
||||
const rawItem = global.localStorage.getItem(FLASH_MESSAGES_LOCAL_STORAGE_KEY);
|
||||
if (rawItem) {
|
||||
// Only try to parse and set flashMessages from the raw item if it exists.
|
||||
const parsed = JSON.parse(rawItem);
|
||||
if (Array.isArray(parsed)) {
|
||||
flashMessages = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If this fails for some reason, just return the empty array.
|
||||
}
|
||||
return flashMessages;
|
||||
}
|
||||
|
||||
function addFlashMessage(message) {
|
||||
try {
|
||||
if (global.localStorage) {
|
||||
const flashMessages = getFlashMessages();
|
||||
flashMessages.push(message);
|
||||
global.localStorage.setItem(FLASH_MESSAGES_LOCAL_STORAGE_KEY, JSON.stringify(flashMessages));
|
||||
}
|
||||
} catch (e) {
|
||||
// If this fails, just bail.
|
||||
let flashMessages = getLocalStorage(FLASH_MESSAGES_LOCAL_STORAGE_KEY);
|
||||
if (!flashMessages || !Array.isArray(flashMessages)) {
|
||||
flashMessages = [];
|
||||
}
|
||||
flashMessages.push(message);
|
||||
setLocalStorage(FLASH_MESSAGES_LOCAL_STORAGE_KEY, flashMessages);
|
||||
}
|
||||
|
||||
function clearFlashMessages() {
|
||||
try {
|
||||
if (global.localStorage) {
|
||||
global.localStorage.removeItem(FLASH_MESSAGES_LOCAL_STORAGE_KEY);
|
||||
}
|
||||
} catch (e) {
|
||||
// If this fails, just bail.
|
||||
}
|
||||
function popFlashMessages() {
|
||||
return popLocalStorage(FLASH_MESSAGES_LOCAL_STORAGE_KEY) || [];
|
||||
}
|
||||
|
||||
export default function UserMessagesProvider({ children }) {
|
||||
@@ -103,12 +73,11 @@ export default function UserMessagesProvider({ children }) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const flashMessages = getFlashMessages();
|
||||
flashMessages.forEach(flashMessage => add(flashMessage));
|
||||
// We only allow flash messages to persist through one refresh, then we clear them out.
|
||||
// If we want persistent messages, then add a 'persist' key to the messages and handle that
|
||||
// as a separate local storage item.
|
||||
clearFlashMessages();
|
||||
const flashMessages = popFlashMessages();
|
||||
flashMessages.forEach(flashMessage => add(flashMessage));
|
||||
}, []);
|
||||
|
||||
const value = {
|
||||
|
||||
Reference in New Issue
Block a user