({
EntitlementBanner: () => 'EntitlementBanner',
}));
jest.mock('./components/CourseCardActions', () => 'CourseCardActions');
+jest.mock('./components/CourseCardDetails', () => 'CourseCardDetails');
const dataProps = {
title: 'hooks.title',
bannerUrl: 'hooks.bannerUrl',
- providerName: 'hooks.providerName',
- accessMessagE: 'hooks.accessMessage',
formatMessage: jest.fn(msg => ({ formatted: msg })),
};
diff --git a/src/containers/CourseCard/messages.js b/src/containers/CourseCard/messages.js
index 4c60a24..d5f0c53 100644
--- a/src/containers/CourseCard/messages.js
+++ b/src/containers/CourseCard/messages.js
@@ -1,36 +1,11 @@
import { StrictDict } from 'utils';
export const messages = StrictDict({
- accessExpired: {
- id: 'learner-dash.courseCard.accessExpired',
- description: 'Course access expiration date message on course card for expired access.',
- defaultMessage: 'Access expired {accessExpirationDate}',
- },
- accessExpires: {
- id: 'learner-dash.courseCard.accessExpires',
- description: 'Course access expiration date message on course card.',
- defaultMessage: 'Access expires {accessExpirationDate}',
- },
- courseEnded: {
- id: 'learner-dash.courseCard.courseEnded',
- description: 'Course ended message on course card.',
- defaultMessage: 'Course ended {endDate}',
- },
- courseEnds: {
- id: 'learner-dash.courseCard.courseEnds',
- description: 'Course ending message on course card.',
- defaultMessage: 'Course ends {endDate}',
- },
bannerAlt: {
id: 'learner-dash.courseCard.bannerAlt',
description: 'Course card banner alt-text',
defaultMessage: 'Course thumbnail',
},
- unknownProviderName: {
- id: 'learner-dash.courseCard.unknownProviderName',
- description: 'Provider name display when name is unknown',
- defaultMessage: 'Unknown',
- },
});
export default messages;
diff --git a/src/containers/CourseList/index.jsx b/src/containers/CourseList/index.jsx
index b427308..1215ea1 100644
--- a/src/containers/CourseList/index.jsx
+++ b/src/containers/CourseList/index.jsx
@@ -2,12 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import CourseCard from 'containers/CourseCard';
+import SelectSession from 'containers/SelectSession';
export const CourseList = ({ courseListData }) => (
{courseListData.map((courseNumber) => (
))}
+
);
diff --git a/src/containers/SelectSession/SelectSessionModal.jsx b/src/containers/SelectSession/SelectSessionModal.jsx
new file mode 100644
index 0000000..72ba23d
--- /dev/null
+++ b/src/containers/SelectSession/SelectSessionModal.jsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+import {
+ ActionRow, Button, Form, ModalDialog,
+} from '@edx/paragon';
+
+import { nullMethod } from 'hooks';
+import { dateFormatter } from 'utils';
+
+import useSelectSession from './hooks';
+import messages from './messages';
+
+export const SelectSessionModal = ({ courseNumber }) => {
+ const {
+ entitlementSessions,
+ showSessionModal,
+ closeSessionModal,
+ showLeaveSessionInSessionModal,
+ courseTitle,
+ } = useSelectSession({
+ courseNumber,
+ });
+
+ const { formatMessage, formatDate } = useIntl();
+
+ let header;
+ let hint;
+ if (showLeaveSessionInSessionModal) {
+ header = formatMessage(messages.changeOrLeaveHeader);
+ hint = formatMessage(messages.changeOrLeaveHint);
+ } else {
+ header = formatMessage(messages.selectSessionHeader, {
+ courseTitle,
+ });
+ hint = formatMessage(messages.selectSessionHint);
+ }
+
+ return (
+
+
+
{header}
+
+ {hint}
+
+ {entitlementSessions?.map((entitle) => (
+
+ {dateFormatter(formatDate, entitle.startDate)} - {dateFormatter(formatDate, entitle.endDate)}
+
+ ))}
+ {showLeaveSessionInSessionModal ? (
+
+ {formatMessage(messages.leaveSessionOption)}
+
+ ) : null}
+
+
+
+
+ {formatMessage(messages.nevermind)}
+
+ {formatMessage(messages.confirmSession)}
+
+
+
+ );
+};
+SelectSessionModal.propTypes = {
+ courseNumber: PropTypes.string.isRequired,
+};
+
+export default SelectSessionModal;
diff --git a/src/containers/SelectSession/SelectSessionModal.test.jsx b/src/containers/SelectSession/SelectSessionModal.test.jsx
new file mode 100644
index 0000000..f8adb2d
--- /dev/null
+++ b/src/containers/SelectSession/SelectSessionModal.test.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import hooks from './hooks';
+import SelectSessionModal from './SelectSessionModal';
+
+jest.mock('./hooks', () => ({
+ __esModule: true,
+ default: jest.fn(),
+}));
+
+const hookReturn = {
+ entitlementSessions: [],
+ showSessionModal: true,
+ closeSessionModal: jest.fn().mockName('useSelectSession.closeSessionModal'),
+ showLeaveSessionInSessionModal: true,
+ courseTitle: 'course-title: unit test save life',
+};
+
+const courseNumber = 'my-test-course-number';
+
+const availableSessions = [
+ { startDate: '1/2/2000', endDate: '1/2/2020', courseNumber },
+ { startDate: '2/3/2000', endDate: '2/3/2020', courseNumber },
+ { startDate: '3/4/2000', endDate: '3/4/2020', courseNumber },
+];
+
+describe('SelectSessionModal', () => {
+ describe('snapshot', () => {
+ test('empty modal with leave option ', () => {
+ hooks.mockReturnValueOnce({
+ ...hookReturn,
+ });
+ expect(shallow( )).toMatchSnapshot();
+ });
+
+ test('modal with leave option ', () => {
+ hooks.mockReturnValueOnce({
+ ...hookReturn,
+ entitlementSessions: [...availableSessions],
+ });
+ expect(shallow( )).toMatchSnapshot();
+ });
+
+ test('modal without leave option ', () => {
+ hooks.mockReturnValueOnce({
+ ...hookReturn,
+ entitlementSessions: [...availableSessions],
+ showLeaveSessionInSessionModal: false,
+ });
+ expect(shallow( )).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/containers/SelectSession/__snapshots__/SelectSessionModal.test.jsx.snap b/src/containers/SelectSession/__snapshots__/SelectSessionModal.test.jsx.snap
new file mode 100644
index 0000000..1e0f09b
--- /dev/null
+++ b/src/containers/SelectSession/__snapshots__/SelectSessionModal.test.jsx.snap
@@ -0,0 +1,188 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SelectSessionModal snapshot empty modal with leave option 1`] = `
+
+
+
+ Change or leave session?
+
+
+
+ When you change to a different session any course progress or grades from your current session will be lost.
+
+
+
+ Leave session
+
+
+
+
+
+ Nevermind
+
+
+ Confirm Session
+
+
+
+
+`;
+
+exports[`SelectSessionModal snapshot modal with leave option 1`] = `
+
+
+
+ Change or leave session?
+
+
+
+ When you change to a different session any course progress or grades from your current session will be lost.
+
+
+
+ 1/2/2000
+ -
+ 1/2/2020
+
+
+ 2/3/2000
+ -
+ 2/3/2020
+
+
+ 3/4/2000
+ -
+ 3/4/2020
+
+
+ Leave session
+
+
+
+
+
+ Nevermind
+
+
+ Confirm Session
+
+
+
+
+`;
+
+exports[`SelectSessionModal snapshot modal without leave option 1`] = `
+
+
+
+ Select a session to access course-title: unit test save life
+
+
+
+ Remember, if you change your mind you have 2 weeks to unenroll and reclaim your entitlement.
+
+
+
+ 1/2/2000
+ -
+ 1/2/2020
+
+
+ 2/3/2000
+ -
+ 2/3/2020
+
+
+ 3/4/2000
+ -
+ 3/4/2020
+
+
+
+
+
+ Nevermind
+
+
+ Confirm Session
+
+
+
+
+`;
diff --git a/src/containers/SelectSession/__snapshots__/index.test.jsx.snap b/src/containers/SelectSession/__snapshots__/index.test.jsx.snap
new file mode 100644
index 0000000..e921de5
--- /dev/null
+++ b/src/containers/SelectSession/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SelectSession snapshot has courseNumber 1`] = `
+
+`;
+
+exports[`SelectSession snapshot no courseNumber 1`] = `""`;
diff --git a/src/containers/SelectSession/hooks.js b/src/containers/SelectSession/hooks.js
new file mode 100644
index 0000000..e7d686d
--- /dev/null
+++ b/src/containers/SelectSession/hooks.js
@@ -0,0 +1,35 @@
+import { hooks as appHooks, actions } from 'data/redux';
+
+import { useDispatch } from 'react-redux';
+
+export const useSelectSession = ({ courseNumber }) => {
+ const dispatch = useDispatch();
+ const {
+ showSessionModal,
+ showLeaveSessionInSessionModal,
+ } = appHooks.useSelectSessionsModalData();
+
+ const { entitlementSessions } = appHooks.useCardEntitlementsData(courseNumber);
+
+ const { title: courseTitle } = appHooks.useCardCourseData(courseNumber);
+
+ const updateSessionModal = (showModal, showLeaveOption = false) => dispatch(
+ actions.app.updateSelectSessionModal({
+ showSessionModal: showModal,
+ showLeaveSessionInSessionModal: showLeaveOption,
+ courseNumber,
+ }),
+ );
+
+ return {
+ showSessionModal,
+ closeSessionModal: () => updateSessionModal(false),
+ openSessionModal: () => updateSessionModal(true),
+ openSessionModalWithLeaveOption: () => updateSessionModal(true, true),
+ showLeaveSessionInSessionModal,
+ entitlementSessions,
+ courseTitle,
+ };
+};
+
+export default useSelectSession;
diff --git a/src/containers/SelectSession/hooks.test.js b/src/containers/SelectSession/hooks.test.js
new file mode 100644
index 0000000..11a7ba2
--- /dev/null
+++ b/src/containers/SelectSession/hooks.test.js
@@ -0,0 +1,85 @@
+import { hooks as appHooks, actions } from 'data/redux';
+
+import * as hooks from './hooks';
+
+jest.mock('data/redux', () => ({
+ hooks: {
+ useCardEntitlementsData: jest.fn(),
+ useCardCourseData: jest.fn(),
+ useSelectSessionsModalData: jest.fn(),
+ },
+ actions: {
+ app: {
+ updateSelectSessionModal: jest.fn(),
+ },
+ },
+}));
+
+const courseNumber = 'my-test-course-number';
+
+const entitlement = {
+ showSessionModal: false,
+ showLeaveSessionInSessionModal: false,
+};
+
+const availableSessions = [
+ { startDate: '1/2/2000', endDate: '1/2/2020', courseNumber },
+ { startDate: '2/3/2000', endDate: '2/3/2020', courseNumber },
+ { startDate: '3/4/2000', endDate: '3/4/2020', courseNumber },
+];
+
+const cardCourseData = {
+ title: 'course-title: brown fox',
+};
+
+describe('SelectSessionModal hooks', () => {
+ let out;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ describe('useSelectSession', () => {
+ beforeEach(() => {
+ appHooks.useSelectSessionsModalData.mockReturnValueOnce({ ...entitlement });
+ appHooks.useCardEntitlementsData.mockReturnValueOnce({ entitlementSessions: availableSessions });
+ appHooks.useCardCourseData.mockReturnValueOnce({ ...cardCourseData });
+ out = hooks.useSelectSession({ courseNumber });
+ });
+
+ test('loads entitlement data based on course number', () => {
+ expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(courseNumber);
+ });
+
+ test('get course title based on course number', () => {
+ expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
+ expect(out.courseTitle).toEqual(cardCourseData.title);
+ });
+
+ test('open session modal', () => {
+ out.openSessionModal();
+ expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({
+ showSessionModal: true,
+ showLeaveSessionInSessionModal: false,
+ courseNumber,
+ });
+ });
+
+ test('open session modal with leave option', () => {
+ out.openSessionModalWithLeaveOption();
+ expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({
+ showSessionModal: true,
+ showLeaveSessionInSessionModal: true,
+ courseNumber,
+ });
+ });
+
+ test('close session modal', () => {
+ out.closeSessionModal();
+ expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({
+ showSessionModal: false,
+ showLeaveSessionInSessionModal: false,
+ courseNumber,
+ });
+ });
+ });
+});
diff --git a/src/containers/SelectSession/index.jsx b/src/containers/SelectSession/index.jsx
new file mode 100644
index 0000000..f4cddca
--- /dev/null
+++ b/src/containers/SelectSession/index.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { hooks as appHooks } from 'data/redux';
+
+import SelectSessionModal from './SelectSessionModal';
+
+export const SelectSession = () => {
+ const { courseNumber } = appHooks.useSelectSessionsModalData();
+ return courseNumber ? : null;
+};
+SelectSession.propTypes = {};
+
+export default SelectSession;
diff --git a/src/containers/SelectSession/index.test.jsx b/src/containers/SelectSession/index.test.jsx
new file mode 100644
index 0000000..8200c0d
--- /dev/null
+++ b/src/containers/SelectSession/index.test.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { hooks as appHooks } from 'data/redux';
+
+import SelectSession from '.';
+
+jest.mock('data/redux', () => ({
+ hooks: {
+ useSelectSessionsModalData: jest.fn(),
+ },
+}));
+
+describe('SelectSession', () => {
+ describe('snapshot', () => {
+ test('no courseNumber', () => {
+ appHooks.useSelectSessionsModalData.mockReturnValueOnce({ courseNumber: null });
+ expect(shallow( )).toMatchSnapshot();
+ });
+
+ test('has courseNumber', () => {
+ appHooks.useSelectSessionsModalData.mockReturnValueOnce({ courseNumber: 'some course' });
+ expect(shallow( )).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/containers/SelectSession/messages.js b/src/containers/SelectSession/messages.js
new file mode 100644
index 0000000..9fa5ed3
--- /dev/null
+++ b/src/containers/SelectSession/messages.js
@@ -0,0 +1,42 @@
+/* eslint-disable quotes */
+import { StrictDict } from 'utils';
+
+export const messages = StrictDict({
+ changeOrLeaveHeader: {
+ id: 'learner-dash.selectSession.changeOrLeaveHeader',
+ description: 'Header for session that allow leave option',
+ defaultMessage: 'Change or leave session?',
+ },
+ selectSessionHeader: {
+ id: 'learner-dash.selectSession.selectSessionHeader',
+ description: 'Header for unfulfilled entitlement',
+ defaultMessage: 'Select a session to access {courseTitle}',
+ },
+ changeOrLeaveHint: {
+ id: 'learner-dash.selectSession.changeOrLeaveHint',
+ description: 'Hint for session that allow leave option',
+ defaultMessage: 'When you change to a different session any course progress or grades from your current session will be lost.',
+ },
+ selectSessionHint: {
+ id: 'learner-dash.selectSession.selectSessionHint',
+ description: 'Hint for session that does not allow leave option',
+ defaultMessage: 'Remember, if you change your mind you have 2 weeks to unenroll and reclaim your entitlement.',
+ },
+ leaveSessionOption: {
+ id: 'learner-dash.selectSession.leaveSessionOption',
+ description: 'Radio option for leave session',
+ defaultMessage: 'Leave session',
+ },
+ nevermind: {
+ id: 'learner-dash.selectSession.nevermind',
+ description: 'Cancel action for select session modal',
+ defaultMessage: 'Nevermind',
+ },
+ confirmSession: {
+ id: 'learner-dash.selectSession.confirmSession',
+ description: 'Confirm action for select session modal',
+ defaultMessage: 'Confirm Session',
+ },
+});
+
+export default messages;
diff --git a/src/data/redux/app/reducer.js b/src/data/redux/app/reducer.js
index 231d1cd..ac44d28 100644
--- a/src/data/redux/app/reducer.js
+++ b/src/data/redux/app/reducer.js
@@ -10,6 +10,7 @@ const initialState = {
platformSettings: {},
suggestedCourses: [],
filterState: {},
+ selectSessionsModal: {},
};
// eslint-disable-next-line no-unused-vars
@@ -41,6 +42,12 @@ const app = createSlice({
platformSettings: payload.platformSettings,
suggestedCourses: payload.suggestedCourses,
}),
+ updateSelectSessionModal: (state, { payload }) => ({
+ ...state,
+ selectSessionsModal: {
+ ...payload,
+ },
+ }),
},
});
diff --git a/src/data/redux/app/reducer.test.js b/src/data/redux/app/reducer.test.js
index f46b2fb..4db6537 100644
--- a/src/data/redux/app/reducer.test.js
+++ b/src/data/redux/app/reducer.test.js
@@ -12,13 +12,13 @@ describe('app reducer', () => {
},
entitlements: [],
};
- const testValue = 'my-test-value';
- const testAction = (action, expected) => {
- expect(reducer(testState, action)).toEqual({
- ...testState,
- ...expected,
- });
- };
+ // const testValue = 'my-test-value';
+ // const testAction = (action, expected) => {
+ // expect(reducer(testState, action)).toEqual({
+ // ...testState,
+ // ...expected,
+ // });
+ // };
describe('action handlers', () => {
describe('loadCourses', () => {
const courseIds = [
diff --git a/src/data/redux/app/selectors.js b/src/data/redux/app/selectors.js
index be393cf..fdae538 100644
--- a/src/data/redux/app/selectors.js
+++ b/src/data/redux/app/selectors.js
@@ -17,6 +17,7 @@ export const simpleSelectors = {
suggestedCourses: mkSimpleSelector(app => app.suggestedCourses),
emailConfirmation: mkSimpleSelector(app => app.emailConfirmation),
enterpriseDashboards: mkSimpleSelector(app => app.enterpriseDashboards),
+ selectSessionsModal: mkSimpleSelector(app => app.selectSessionsModal),
};
export const courseCardData = (state, courseNumber) => (
@@ -68,6 +69,7 @@ export const courseCard = StrictDict({
const showExpirationWarning = deadline > new Date() && deadline <= dateSixMonthsFromNow;
return {
canChange: entitlements.canChange,
+ canViewCourse: entitlements.canViewCourse,
entitlementSessions: entitlements.availableSessions,
isEntitlement: entitlements.isEntitlement,
isExpired: entitlements.isExpired,
diff --git a/src/data/redux/hooks.js b/src/data/redux/hooks.js
index ef915e9..7135a32 100644
--- a/src/data/redux/hooks.js
+++ b/src/data/redux/hooks.js
@@ -9,6 +9,7 @@ export const useEnterpriseDashboardData = () => useSelector(appSelectors.enterpr
export const usePlatformSettingsData = () => useSelector(appSelectors.platformSettings);
// suggested courses is max at 3 at the moment.
export const useSuggestedCoursesData = () => useSelector(appSelectors.suggestedCourses).slice(0, 3);
+export const useSelectSessionsModalData = () => useSelector(appSelectors.selectSessionsModal);
// eslint-disable-next-line
export const useCourseCardData = (selector) => (courseNumber) => useSelector(
diff --git a/src/data/services/lms/fakeData/courses.js b/src/data/services/lms/fakeData/courses.js
index 995aec0..2f8000e 100644
--- a/src/data/services/lms/fakeData/courses.js
+++ b/src/data/services/lms/fakeData/courses.js
@@ -289,6 +289,7 @@ export const courseRuns = [
canChange: true,
changeDeadline: futureDate,
isExpired: false,
+ availableSessions,
},
},
// Entitlement Course Run - Can View and Change
@@ -305,6 +306,7 @@ export const courseRuns = [
canChange: true,
changeDeadline: futureDate,
isExpired: false,
+ availableSessions,
},
},
// Entitlement Course Run - Can View but not Change
@@ -345,6 +347,10 @@ export const courseRuns = [
},
];
+// unfulfilled entitlement select session
+// unfulfilled entitlement select session with deadline
+// unfulfilled entitlement select session pass deadline with available session {banner different from 4th}
+// unfulfilled entitlement select session pass deadline without available session
export const entitlementCourses = [
{
entitlements: {
diff --git a/src/setupTest.jsx b/src/setupTest.jsx
index 2629fd3..a092edf 100755
--- a/src/setupTest.jsx
+++ b/src/setupTest.jsx
@@ -19,7 +19,7 @@ jest.mock('@edx/frontend-platform/i18n', () => {
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
const PropTypes = jest.requireActual('prop-types');
const { formatMessage } = jest.requireActual('./testUtils');
- const formatDate = jest.fn(date => date).mockName('useIntl.formatDate');
+ const formatDate = jest.fn(date => new Date(date).toLocaleDateString()).mockName('useIntl.formatDate');
return {
...i18n,
intlShape: PropTypes.shape({
@@ -133,8 +133,6 @@ jest.mock('hooks', () => ({
nullMethod: jest.fn().mockName('hooks.nullMethod'),
}));
-jest.mock('@zip.js/zip.js', () => ({}));
-
// Mock react-redux hooks
// unmock for integration tests
jest.mock('react-redux', () => {
@@ -154,3 +152,10 @@ jest.mock('hooks', () => ({
...jest.requireActual('hooks'),
nullMethod: jest.fn().mockName('hooks.nullMethod'),
}));
+
+jest.mock('moment', () => ({
+ __esModule: true,
+ default: (date) => ({
+ toDate: jest.fn().mockReturnValue(date),
+ }),
+}));
diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx
index 2d2f78a..e12f513 100644
--- a/src/test/app.test.jsx
+++ b/src/test/app.test.jsx
@@ -118,14 +118,13 @@ describe('ESG app integration tests', () => {
inspector = new Inspector(el);
});
- test('initialization', async (done) => {
+ test('initialization', async () => {
await waitForRequestStatus(RequestKeys.initialize, RequestStates.pending);
resolveFns.init.success();
await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed);
- done();
});
- test('course cards', async (done) => {
+ test('course cards', async () => {
resolveFns.init.success();
await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed);
await inspector.findByText(fakeData.courseRunData[0].course.title);
@@ -152,11 +151,9 @@ describe('ESG app integration tests', () => {
[
courseData.provider.name,
courseNumber,
- appMessages.withValues.CourseCard.accessExpires({
+ appMessages.withValues.CourseCardDetails.accessExpires({
accessExpirationDate: courseData.enrollment.accessExpirationDate,
}),
].forEach(value => inspector.verifyTextIncludes(cardDetails, value));
-
- done();
});
});
diff --git a/src/test/messages.js b/src/test/messages.js
index 3f00630..16bf61c 100644
--- a/src/test/messages.js
+++ b/src/test/messages.js
@@ -1,4 +1,4 @@
-import CourseCard from 'containers/CourseCard/messages';
+import CourseCardDetails from 'containers/CourseCard/components/CourseCardDetails/messages';
const mapMessages = (messages) => Object.keys(messages).reduce(
(acc, key) => ({ ...acc, [key]: messages[key].defaultMessage }),
@@ -22,8 +22,8 @@ const mapMessagesWithValues = (messages) => Object.keys(messages).reduce(
);
export default {
- CourseCard: mapMessages(CourseCard),
+ CourseCardDetails: mapMessages(CourseCardDetails),
withValues: {
- CourseCard: mapMessagesWithValues(CourseCard),
+ CourseCardDetails: mapMessagesWithValues(CourseCardDetails),
},
};
diff --git a/src/utils/dateFormatter.js b/src/utils/dateFormatter.js
new file mode 100644
index 0000000..c9f007b
--- /dev/null
+++ b/src/utils/dateFormatter.js
@@ -0,0 +1,5 @@
+import moment from 'moment';
+
+const dateFormatter = (formatDate, date) => formatDate(moment(date).toDate(), { year: 'numeric', month: 'long', day: '2-digit' });
+
+export default dateFormatter;
diff --git a/src/utils/index.js b/src/utils/index.js
index 6108258..0db13b4 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -1,2 +1,3 @@
export { default as StrictDict } from './StrictDict';
export { default as keyStore } from './keyStore';
+export { default as dateFormatter } from './dateFormatter';