diff --git a/package-lock.json b/package-lock.json
index 962373c..090a811 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "@edx/frontend-app-learner-dash",
+ "name": "@edx/frontend-app-learner-dashboard",
"version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "@edx/frontend-app-learner-dash",
+ "name": "@edx/frontend-app-learner-dashboard",
"version": "0.0.1",
"license": "AGPL-3.0",
"dependencies": {
@@ -13,7 +13,7 @@
"@edx/browserslist-config": "^1.1.0",
"@edx/frontend-component-footer": "^11.4.1",
"@edx/frontend-platform": "^2.6.2",
- "@edx/paragon": "20.12.0",
+ "@edx/paragon": "20.19.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
@@ -2604,9 +2604,9 @@
}
},
"node_modules/@edx/paragon": {
- "version": "20.12.0",
- "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.12.0.tgz",
- "integrity": "sha512-0BRsKjSWJdUYV2c0OHiYon7beIxL8uhlyyYpJVyVI6dDRGr5SnJufPaRXdo5L9NUpakDYo+SWr59DL1t5/0Q4Q==",
+ "version": "20.19.0",
+ "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.19.0.tgz",
+ "integrity": "sha512-N35cTPOrpacUKEfl8L2hryPzmPBGNbLRSgl/+BAIxyJuvn5etAjsAw6Etz3M91yRaTGLYH7+9HtExLES+qNfXw==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
@@ -2634,7 +2634,8 @@
},
"peerDependencies": {
"react": "^16.8.6 || ^17.0.0",
- "react-dom": "^16.8.6 || ^17.0.0"
+ "react-dom": "^16.8.6 || ^17.0.0",
+ "react-intl": "^5.25.1"
}
},
"node_modules/@edx/paragon/node_modules/@fortawesome/fontawesome-common-types": {
@@ -32218,9 +32219,9 @@
}
},
"@edx/paragon": {
- "version": "20.12.0",
- "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.12.0.tgz",
- "integrity": "sha512-0BRsKjSWJdUYV2c0OHiYon7beIxL8uhlyyYpJVyVI6dDRGr5SnJufPaRXdo5L9NUpakDYo+SWr59DL1t5/0Q4Q==",
+ "version": "20.19.0",
+ "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.19.0.tgz",
+ "integrity": "sha512-N35cTPOrpacUKEfl8L2hryPzmPBGNbLRSgl/+BAIxyJuvn5etAjsAw6Etz3M91yRaTGLYH7+9HtExLES+qNfXw==",
"requires": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
diff --git a/package.json b/package.json
index fc5584a..6e0a917 100755
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
{
- "name": "@edx/frontend-app-learner-dash",
+ "name": "@edx/frontend-app-learner-dashboard",
"version": "0.0.1",
"description": "",
"repository": {
"type": "git",
- "url": "git+https://github.com/edx/frontend-app-learner-dash.git"
+ "url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
},
"scripts": {
"build": "fedx-scripts webpack",
@@ -31,7 +31,7 @@
"@edx/browserslist-config": "^1.1.0",
"@edx/frontend-component-footer": "^11.4.1",
"@edx/frontend-platform": "^2.6.2",
- "@edx/paragon": "20.12.0",
+ "@edx/paragon": "20.19.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
diff --git a/src/__snapshots__/index.test.jsx.snap b/src/__snapshots__/index.test.jsx.snap
index 1e8cd8e..cf7b63d 100644
--- a/src/__snapshots__/index.test.jsx.snap
+++ b/src/__snapshots__/index.test.jsx.snap
@@ -1,9 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
-
+
+
+
`;
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
diff --git a/src/config/index.js b/src/config/index.js
index 98dfc88..9ada1d1 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -7,7 +7,7 @@ const configuration = {
// REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
// DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
// SECURE_COOKIES: process.env.NODE_ENV !== 'development',
- // SEGMENT_KEY: process.env.SEGMENT_KEY,
+ SEGMENT_KEY: process.env.SEGMENT_KEY,
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
PERSONALIZED_RECOMMENDATION_COOKIE_NAME: process.env.PERSONALIZED_RECOMMENDATION_COOKIE_NAME || '',
diff --git a/src/containers/CourseCard/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/__snapshots__/index.test.jsx.snap
index 1fba43a..84cd781 100644
--- a/src/containers/CourseCard/__snapshots__/index.test.jsx.snap
+++ b/src/containers/CourseCard/__snapshots__/index.test.jsx.snap
@@ -13,10 +13,41 @@ exports[`CourseCard component snapshot: collapsed 1`] = `
className="d-flex flex-column w-100"
>
-
+
+
+ }
+ title={
+
+ }
+ />
+
+
+
+
+
+
+
+
-
+
+
+ }
+ title={
+
+ }
+ />
+
+
+
+
+
+
+
+
{
+ const { formatMessage } = useIntl();
+
const { upgradeUrl } = hooks.useCardCourseRunData(cardId);
const { canUpgrade } = hooks.useCardEnrollmentData(cardId);
const { isMasquerading } = hooks.useMasqueradeData();
- const { formatMessage } = useIntl();
- const isEnabled = (!isMasquerading && canUpgrade);
- const { trackUpgradeClick } = useTrackUpgradeData();
+ const trackUpgradeClick = hooks.useTrackCourseEvent(
+ track.course.upgradeClicked,
+ cardId,
+ upgradeUrl,
+ );
+ const isEnabled = (!isMasquerading && canUpgrade);
+ const enabledProps = {
+ as: 'a',
+ href: upgradeUrl,
+ onClick: trackUpgradeClick,
+ };
return (
{formatMessage(messages.upgrade)}
diff --git a/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx b/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx
index b28c69f..d9b6167 100644
--- a/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx
+++ b/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx
@@ -1,20 +1,28 @@
import { shallow } from 'enzyme';
-import { htmlProps } from 'data/constants/htmlKeys';
+import track from 'tracking';
import { hooks } from 'data/redux';
+import { htmlProps } from 'data/constants/htmlKeys';
import UpgradeButton from './UpgradeButton';
+jest.mock('tracking', () => ({
+ course: {
+ upgradeClicked: jest.fn().mockName('segment.trackUpgradeClicked'),
+ },
+}));
+
jest.mock('data/redux', () => ({
hooks: {
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(() => ({ canUpgrade: true })),
+ useTrackCourseEvent: jest.fn(
+ (eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
+ ),
},
}));
+
jest.mock('./ActionButton', () => 'ActionButton');
-jest.mock('./hooks', () => () => ({
- trackUpgradeClick: jest.fn().mockName('trackUpgradeClick'),
-}));
describe('UpgradeButton', () => {
const props = {
@@ -27,6 +35,11 @@ describe('UpgradeButton', () => {
const wrapper = shallow(
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
+ expect(wrapper.prop(htmlProps.onClick)).toEqual(hooks.useTrackCourseEvent(
+ track.course.upgradeClicked,
+ props.cardId,
+ upgradeUrl,
+ ));
});
test('cannot upgrade', () => {
hooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: false });
diff --git a/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.jsx b/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.jsx
index 5f9e3da..7844793 100644
--- a/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.jsx
+++ b/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
+import track from 'tracking';
import { hooks } from 'data/redux';
import ActionButton from './ActionButton';
import messages from './messages';
@@ -10,12 +11,19 @@ import messages from './messages';
export const ViewCourseButton = ({ cardId }) => {
const { homeUrl } = hooks.useCardCourseRunData(cardId);
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
+ const handleClick = hooks.useTrackCourseEvent(
+ track.course.enterCourseClicked,
+ cardId,
+ homeUrl,
+ );
const { formatMessage } = useIntl();
+
return (
{formatMessage(messages.viewCourse)}
diff --git a/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.test.jsx b/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.test.jsx
index 0f88968..1d67015 100644
--- a/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.test.jsx
+++ b/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.test.jsx
@@ -1,14 +1,24 @@
import { shallow } from 'enzyme';
+import track from 'tracking';
import { htmlProps } from 'data/constants/htmlKeys';
import { hooks } from 'data/redux';
import ViewCourseButton from './ViewCourseButton';
+jest.mock('tracking', () => ({
+ course: {
+ enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
+ },
+}));
+
jest.mock('data/redux', () => ({
hooks: {
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardEntitlementData: jest.fn(),
+ useTrackCourseEvent: jest.fn(
+ (eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
+ ),
},
}));
jest.mock('./ActionButton', () => 'ActionButton');
@@ -38,7 +48,11 @@ describe('ViewCourseButton', () => {
expect(wrapper).toMatchSnapshot();
});
test('links to home URL', () => {
- expect(wrapper.prop(htmlProps.href)).toEqual(homeUrl);
+ expect(wrapper.prop(htmlProps.onClick)).toEqual(hooks.useTrackCourseEvent(
+ track.course.enterCourseClicked,
+ defaultProps.cardId,
+ homeUrl,
+ ));
});
test('link is enabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
@@ -52,7 +66,11 @@ describe('ViewCourseButton', () => {
expect(wrapper).toMatchSnapshot();
});
test('links to home URL', () => {
- expect(wrapper.prop(htmlProps.href)).toEqual(homeUrl);
+ expect(wrapper.prop(htmlProps.onClick)).toEqual(hooks.useTrackCourseEvent(
+ track.course.enterCourseClicked,
+ defaultProps.cardId,
+ homeUrl,
+ ));
});
test('link is enabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
diff --git a/src/containers/CourseCard/components/CourseCardActions/__snapshots__/UpgradeButton.test.jsx.snap b/src/containers/CourseCard/components/CourseCardActions/__snapshots__/UpgradeButton.test.jsx.snap
index b5b2077..4e250f1 100644
--- a/src/containers/CourseCard/components/CourseCardActions/__snapshots__/UpgradeButton.test.jsx.snap
+++ b/src/containers/CourseCard/components/CourseCardActions/__snapshots__/UpgradeButton.test.jsx.snap
@@ -6,7 +6,15 @@ exports[`UpgradeButton snapshot can upgrade 1`] = `
disabled={false}
href="upgradeUrl"
iconBefore={[MockFunction icons.Locked]}
- onClick={[MockFunction trackUpgradeClick]}
+ onClick={
+ Object {
+ "trackCourseEvent": Object {
+ "cardId": "cardId",
+ "eventName": [MockFunction segment.trackUpgradeClicked],
+ "upgradeUrl": "upgradeUrl",
+ },
+ }
+ }
variant="outline-primary"
>
Upgrade
@@ -17,7 +25,6 @@ exports[`UpgradeButton snapshot cannot upgrade 1`] = `
Upgrade
@@ -28,7 +35,6 @@ exports[`UpgradeButton snapshot masquerading 1`] = `
Upgrade
diff --git a/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ViewCourseButton.test.jsx.snap b/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ViewCourseButton.test.jsx.snap
index da92a1f..89b6b80 100644
--- a/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ViewCourseButton.test.jsx.snap
+++ b/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ViewCourseButton.test.jsx.snap
@@ -4,7 +4,16 @@ exports[`ViewCourseButton learner does not have access to course snapshot 1`] =
View Course
@@ -14,7 +23,16 @@ exports[`ViewCourseButton learner has access to course snapshot 1`] = `
View Course
diff --git a/src/containers/CourseCard/components/CourseCardActions/hooks.js b/src/containers/CourseCard/components/CourseCardActions/hooks.js
deleted file mode 100644
index c05c94e..0000000
--- a/src/containers/CourseCard/components/CourseCardActions/hooks.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { handleEvent } from 'data/services/segment/utils';
-import { eventNames } from 'data/services/segment/constants';
-
-export const useTrackUpgradeData = () => {
- const trackUpgradeClick = () => {
- handleEvent(eventNames.upgradeCourse, {
- pageName: 'learner_home',
- linkType: 'button',
- linkCategory: 'green_upgrade',
- });
- };
-
- return {
- trackUpgradeClick,
- };
-};
-
-export default useTrackUpgradeData;
diff --git a/src/containers/CourseCard/components/CourseCardActions/hooks.test.js b/src/containers/CourseCard/components/CourseCardActions/hooks.test.js
deleted file mode 100644
index 26b5e4e..0000000
--- a/src/containers/CourseCard/components/CourseCardActions/hooks.test.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { handleEvent } from 'data/services/segment/utils';
-import { eventNames } from 'data/services/segment/constants';
-import * as hooks from './hooks';
-
-jest.mock('data/services/segment/utils', () => ({
- handleEvent: jest.fn(),
-}));
-
-describe('CourseCardActions hooks', () => {
- describe('useTrackUpgradeData', () => {
- it('calls handleEvent with correct params', () => {
- const out = hooks.useTrackUpgradeData();
- out.trackUpgradeClick();
- expect(handleEvent).toHaveBeenCalledWith(eventNames.upgradeCourse, {
- pageName: 'learner_home',
- linkType: 'button',
- linkCategory: 'green_upgrade',
- });
- });
- });
-});
diff --git a/src/containers/CourseCard/components/CourseCardContent.jsx b/src/containers/CourseCard/components/CourseCardContent.jsx
deleted file mode 100644
index 5939ed5..0000000
--- a/src/containers/CourseCard/components/CourseCardContent.jsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { useIntl } from '@edx/frontend-platform/i18n';
-
-// import PropTypes from 'prop-types';
-import { Card, Badge } from '@edx/paragon';
-
-import { hooks as appHooks } from 'data/redux';
-
-import verifiedRibbon from 'assets/verified-ribbon.png';
-import RelatedProgramsBadge from './RelatedProgramsBadge';
-import CourseCardMenu from './CourseCardMenu';
-import CourseCardActions from './CourseCardActions';
-import CourseCardDetails from './CourseCardDetails';
-
-import messages from '../messages';
-
-export const CourseCardContent = ({ cardId, orientation }) => {
- const { formatMessage } = useIntl();
- const { courseName, bannerImgSrc } = appHooks.useCardCourseData(cardId);
- const { homeUrl } = appHooks.useCardCourseRunData(cardId);
- const { isVerified } = appHooks.useCardEnrollmentData(cardId);
- return (
- <>
-
-
- {
- isVerified && (
-
- {formatMessage(messages.verifiedBanner)}
-
-
- )
- }
-
-
-
-
- {courseName}
-
-
- )}
- actions={}
- />
-
-
-
-
-
-
-
-
- >
- );
-};
-
-CourseCardContent.propTypes = {
- cardId: PropTypes.string.isRequired,
- orientation: PropTypes.string.isRequired,
-};
-
-CourseCardContent.defaultProps = {};
-
-export default CourseCardContent;
diff --git a/src/containers/CourseCard/components/CourseCardContent.test.jsx b/src/containers/CourseCard/components/CourseCardContent.test.jsx
deleted file mode 100644
index ae2c8b7..0000000
--- a/src/containers/CourseCard/components/CourseCardContent.test.jsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { shallow } from 'enzyme';
-
-import { hooks } from 'data/redux';
-import CourseCardContent from './CourseCardContent';
-
-jest.mock('data/redux', () => ({
- hooks: {
- useCardCourseData: jest.fn(),
- useCardCourseRunData: jest.fn(),
- useCardEnrollmentData: jest.fn(),
- },
-}));
-
-jest.mock('./CourseCardActions', () => 'CourseCardActions');
-jest.mock('./CourseCardDetails', () => 'CourseCardDetails');
-jest.mock('./RelatedProgramsBadge', () => 'RelatedProgramsBadge');
-jest.mock('./CourseCardMenu', () => 'CourseCardMenu');
-
-describe('CourseCardContent', () => {
- const props = {
- cardId: 'test-card-id',
- orientation: 'vertical',
- };
- hooks.useCardCourseData.mockReturnValue({
- courseName: 'test-course-name',
- bannerImgSrc: 'test-banner-img-src',
- });
- hooks.useCardCourseRunData.mockReturnValue({
- homeUrl: 'test-home-url',
- });
- describe('snapshot', () => {
- test('orientation vertical', () => {
- hooks.useCardEnrollmentData.mockReturnValue({
- isVerified: true,
- });
- const wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
- });
- test('orientation horizontal', () => {
- hooks.useCardEnrollmentData.mockReturnValue({
- isVerified: true,
- });
- const wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
- });
- test('not verified', () => {
- hooks.useCardEnrollmentData.mockReturnValue({
- isVerified: false,
- });
- const wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
- });
- });
-});
diff --git a/src/containers/CourseCard/components/CourseCardImage.jsx b/src/containers/CourseCard/components/CourseCardImage.jsx
new file mode 100644
index 0000000..3be032e
--- /dev/null
+++ b/src/containers/CourseCard/components/CourseCardImage.jsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { Badge } from '@edx/paragon';
+
+import track from 'tracking';
+import { hooks as appHooks } from 'data/redux';
+
+import verifiedRibbon from 'assets/verified-ribbon.png';
+
+import messages from '../messages';
+
+const { courseImageClicked } = track.course;
+
+export const CourseCardImage = ({ cardId, orientation }) => {
+ const { formatMessage } = useIntl();
+ const { bannerImgSrc } = appHooks.useCardCourseData(cardId);
+ const { homeUrl } = appHooks.useCardCourseRunData(cardId);
+ const { isVerified } = appHooks.useCardEnrollmentData(cardId);
+ const { isEntitlement } = appHooks.useCardEntitlementData(cardId);
+ const handleImageClicked = appHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
+ const image = (
+ <>
+
+ {
+ isVerified && (
+
+
+ {formatMessage(messages.verifiedBanner)}
+
+
+
+ )
+ }
+ >
+ );
+ return isEntitlement
+ ? image
+ : (
+
+ {image}
+
+ );
+};
+CourseCardImage.propTypes = {
+ cardId: PropTypes.string.isRequired,
+ orientation: PropTypes.string.isRequired,
+};
+
+CourseCardImage.defaultProps = {};
+
+export default CourseCardImage;
diff --git a/src/containers/CourseCard/components/CourseCardLayout.jsx b/src/containers/CourseCard/components/CourseCardLayout.jsx
deleted file mode 100644
index 38246e4..0000000
--- a/src/containers/CourseCard/components/CourseCardLayout.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { Card } from '@edx/paragon';
-
-import { useIsCollapsed } from '../hooks';
-import CourseCardBanners from './CourseCardBanners';
-import CourseCardContent from './CourseCardContent';
-
-export const CourseCardLayout = ({
- cardId,
-}) => {
- const isCollapsed = useIsCollapsed();
- return (
-
- );
-};
-CourseCardLayout.propTypes = {
- cardId: PropTypes.string.isRequired,
-};
-
-export default CourseCardLayout;
diff --git a/src/containers/CourseCard/components/CourseCardLayout.test.jsx b/src/containers/CourseCard/components/CourseCardLayout.test.jsx
deleted file mode 100644
index 7be493d..0000000
--- a/src/containers/CourseCard/components/CourseCardLayout.test.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { shallow } from 'enzyme';
-
-import CourseCardLayout from './CourseCardLayout';
-import { useIsCollapsed } from '../hooks';
-
-jest.mock('../hooks', () => ({
- useIsCollapsed: jest.fn(),
-}));
-
-jest.mock('./CourseCardBanners', () => 'CourseCardBanners');
-jest.mock('./CourseCardContent', () => 'CourseCardContent');
-
-describe('CourseCardLayout', () => {
- const props = {
- cardId: 'test-card-id',
- };
- describe('snapshot', () => {
- test('is collapsed', () => {
- useIsCollapsed.mockReturnValue(true);
- const wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
- });
- test('is not collapsed', () => {
- useIsCollapsed.mockReturnValue(false);
- const wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
- });
- });
-});
diff --git a/src/containers/CourseCard/components/CourseCardMenu/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/components/CourseCardMenu/__snapshots__/index.test.jsx.snap
index d4c6064..44fe18c 100644
--- a/src/containers/CourseCard/components/CourseCardMenu/__snapshots__/index.test.jsx.snap
+++ b/src/containers/CourseCard/components/CourseCardMenu/__snapshots__/index.test.jsx.snap
@@ -28,6 +28,7 @@ exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1
{
isVisible,
};
};
+
+export const useHandleToggleDropdown = (cardId) => {
+ const eventName = track.course.courseOptionsDropdownClicked;
+ const trackCourseEvent = appHooks.useTrackCourseEvent(eventName, cardId);
+ return (isOpen) => {
+ if (isOpen) { trackCourseEvent(); }
+ };
+};
diff --git a/src/containers/CourseCard/components/CourseCardMenu/index.jsx b/src/containers/CourseCard/components/CourseCardMenu/index.jsx
index 1e5f429..87bd4d9 100644
--- a/src/containers/CourseCard/components/CourseCardMenu/index.jsx
+++ b/src/containers/CourseCard/components/CourseCardMenu/index.jsx
@@ -6,28 +6,38 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown, Icon, IconButton } from '@edx/paragon';
import { MoreVert } from '@edx/paragon/icons';
+import track from 'tracking';
import { hooks as appHooks } from 'data/redux';
import EmailSettingsModal from 'containers/EmailSettingsModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
-import { useEmailSettings, useUnenrollData } from './hooks';
+import {
+ useEmailSettings,
+ useUnenrollData,
+ useHandleToggleDropdown,
+} from './hooks';
import messages from './messages';
export const CourseCardMenu = ({ cardId }) => {
- const emailSettingsModal = useEmailSettings();
- const unenrollModal = useUnenrollData();
+ const { formatMessage } = useIntl();
+
const { courseName } = appHooks.useCardCourseData(cardId);
const { isEnrolled, isEmailEnabled } = appHooks.useCardEnrollmentData(cardId);
- const {
- // facebook,
- twitter,
- } = appHooks.useCardSocialSettingsData(cardId);
+ const { twitter } = appHooks.useCardSocialSettingsData(cardId);
const { isMasquerading } = appHooks.useMasqueradeData();
- const { formatMessage } = useIntl();
+ const handleTwitterShare = appHooks.useTrackCourseEvent(
+ track.socialShare,
+ cardId,
+ 'twitter',
+ );
+
+ const emailSettingsModal = useEmailSettings();
+ const unenrollModal = useUnenrollData();
+ const handleToggleDropdown = useHandleToggleDropdown(cardId);
return (
<>
-
+
{
{twitter.isEnabled && (
({
useCardEnrollmentData: jest.fn(),
useCardSocialSettingsData: jest.fn(),
useMasqueradeData: jest.fn(),
+ useTrackCourseEvent: jest.fn(),
},
}));
jest.mock('./hooks', () => ({
useEmailSettings: jest.fn(),
useUnenrollData: jest.fn(),
+ useHandleToggleDropdown: jest.fn(),
}));
const props = {
@@ -58,6 +60,7 @@ describe('CourseCardMenu', () => {
appHooks.useCardCourseData.mockReturnValue({ courseName });
appHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: true, isEmailEnabled: true });
appHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
+ appHooks.useTrackCourseEvent.mockReturnValue(jest.fn().mockName('handleTwitterShare'));
});
describe('enrolled, share enabled, email setting enable', () => {
beforeEach(() => {
diff --git a/src/containers/CourseCard/components/CourseCardTitle.jsx b/src/containers/CourseCard/components/CourseCardTitle.jsx
new file mode 100644
index 0000000..812224e
--- /dev/null
+++ b/src/containers/CourseCard/components/CourseCardTitle.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import track from 'tracking';
+import { hooks as appHooks } from 'data/redux';
+
+const { courseTitleClicked } = track.course;
+
+export const CourseCardTitle = ({ cardId }) => {
+ const { courseName } = appHooks.useCardCourseData(cardId);
+ const { homeUrl } = appHooks.useCardCourseRunData(cardId);
+ const handleTitleClicked = appHooks.useTrackCourseEvent(courseTitleClicked, cardId, homeUrl);
+ return (
+
+ );
+};
+
+CourseCardTitle.propTypes = {
+ cardId: PropTypes.string.isRequired,
+};
+
+CourseCardTitle.defaultProps = {};
+
+export default CourseCardTitle;
diff --git a/src/containers/CourseCard/components/__snapshots__/CourseCardContent.test.jsx.snap b/src/containers/CourseCard/components/__snapshots__/CourseCardContent.test.jsx.snap
deleted file mode 100644
index 26d6449..0000000
--- a/src/containers/CourseCard/components/__snapshots__/CourseCardContent.test.jsx.snap
+++ /dev/null
@@ -1,189 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`CourseCardContent snapshot not verified 1`] = `
-
-
-
-
-
-
- }
- title={
-
- }
- />
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`CourseCardContent snapshot orientation horizontal 1`] = `
-
-
-
-
-
- Verified
-
-
-
-
-
-
- }
- title={
-
- }
- />
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`CourseCardContent snapshot orientation vertical 1`] = `
-
-
-
-
-
- Verified
-
-
-
-
-
-
- }
- title={
-
- }
- />
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/containers/CourseCard/components/__snapshots__/CourseCardLayout.test.jsx.snap b/src/containers/CourseCard/components/__snapshots__/CourseCardLayout.test.jsx.snap
deleted file mode 100644
index bca8f23..0000000
--- a/src/containers/CourseCard/components/__snapshots__/CourseCardLayout.test.jsx.snap
+++ /dev/null
@@ -1,63 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`CourseCardLayout snapshot is collapsed 1`] = `
-
-`;
-
-exports[`CourseCardLayout snapshot is not collapsed 1`] = `
-
-`;
diff --git a/src/containers/CourseCard/index.jsx b/src/containers/CourseCard/index.jsx
index ac9c102..ccde230 100644
--- a/src/containers/CourseCard/index.jsx
+++ b/src/containers/CourseCard/index.jsx
@@ -1,12 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
-// import PropTypes from 'prop-types';
import { Card } from '@edx/paragon';
import { useIsCollapsed } from './hooks';
-import CourseCardContent from './components/CourseCardContent';
import CourseCardBanners from './components/CourseCardBanners';
+import CourseCardImage from './components/CourseCardImage';
+import CourseCardMenu from './components/CourseCardMenu';
+import CourseCardActions from './components/CourseCardActions';
+import CourseCardDetails from './components/CourseCardDetails';
+import CourseCardTitle from './components/CourseCardTitle';
+import RelatedProgramsBadge from './components/RelatedProgramsBadge';
import './CourseCard.scss';
@@ -20,7 +24,20 @@ export const CourseCard = ({
-
+
+
+ }
+ actions={}
+ />
+
+
+
+
+
+
+
+
diff --git a/src/containers/CourseCard/index.test.jsx b/src/containers/CourseCard/index.test.jsx
index 68aff70..5f09d5e 100644
--- a/src/containers/CourseCard/index.test.jsx
+++ b/src/containers/CourseCard/index.test.jsx
@@ -9,7 +9,12 @@ jest.mock('./hooks', () => ({
}));
jest.mock('./components/CourseCardBanners', () => 'CourseCardBanners');
-jest.mock('./components/CourseCardContent', () => 'CourseCardContent');
+jest.mock('./components/CourseCardImage', () => 'CourseCardImage');
+jest.mock('./components/CourseCardMenu', () => 'CourseCardMenu');
+jest.mock('./components/CourseCardActions', () => 'CourseCardActions');
+jest.mock('./components/CourseCardDetails', () => 'CourseCardDetails');
+jest.mock('./components/CourseCardTitle', () => 'CourseCardTitle');
+jest.mock('./components/RelatedProgramsBadge', () => 'RelatedProgramsBadge');
const cardId = 'test-card-id';
diff --git a/src/containers/EnterpriseDashboardModal/__snapshots__/index.test.jsx.snap b/src/containers/EnterpriseDashboardModal/__snapshots__/index.test.jsx.snap
index fae0a28..53e48cf 100644
--- a/src/containers/EnterpriseDashboardModal/__snapshots__/index.test.jsx.snap
+++ b/src/containers/EnterpriseDashboardModal/__snapshots__/index.test.jsx.snap
@@ -1,9 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`EnterpriseDashboard empty snapshot 1`] = `""`;
+
exports[`EnterpriseDashboard snapshot 1`] = `
@@ -56,7 +56,6 @@ LoadedView.propTypes = {
marketingUrl: PropTypes.string,
})).isRequired,
isPersonalizedRecommendation: PropTypes.bool.isRequired,
- courseSearchClickTracker: PropTypes.func.isRequired,
};
export default LoadedView;
diff --git a/src/widgets/RecommendationsPanel/LoadedView.test.jsx b/src/widgets/RecommendationsPanel/LoadedView.test.jsx
index ef9928b..2f5cb14 100644
--- a/src/widgets/RecommendationsPanel/LoadedView.test.jsx
+++ b/src/widgets/RecommendationsPanel/LoadedView.test.jsx
@@ -13,12 +13,14 @@ jest.mock('data/redux', () => ({
}),
},
}));
+jest.mock('./track', () => ({
+ findCoursesClicked: () => 'find-courses-clicked',
+}));
describe('RecommendationsPanel LoadedView', () => {
const props = {
courses: mockData.courses,
isPersonalizedRecommendation: false,
- courseSearchClickTracker: jest.fn().mockName('courseSearchClickTracker'),
};
describe('snapshot', () => {
test('without personalize recommendation', () => {
diff --git a/src/widgets/RecommendationsPanel/__snapshots__/LoadedView.test.jsx.snap b/src/widgets/RecommendationsPanel/__snapshots__/LoadedView.test.jsx.snap
index b2b3309..d074753 100644
--- a/src/widgets/RecommendationsPanel/__snapshots__/LoadedView.test.jsx.snap
+++ b/src/widgets/RecommendationsPanel/__snapshots__/LoadedView.test.jsx.snap
@@ -65,7 +65,7 @@ exports[`RecommendationsPanel LoadedView snapshot with personalize recommendatio
Explore courses
@@ -81,7 +81,7 @@ exports[`RecommendationsPanel LoadedView snapshot without personalize recommenda
- Popular on edX
+ Popular courses
Explore courses
diff --git a/src/widgets/RecommendationsPanel/components/CourseCard.jsx b/src/widgets/RecommendationsPanel/components/CourseCard.jsx
index 49edbea..ad6ea70 100644
--- a/src/widgets/RecommendationsPanel/components/CourseCard.jsx
+++ b/src/widgets/RecommendationsPanel/components/CourseCard.jsx
@@ -4,27 +4,19 @@ import PropTypes from 'prop-types';
import { Card, Hyperlink, Truncate } from '@edx/paragon';
import { useIsCollapsed } from 'containers/CourseCard/hooks';
-import { configuration } from '../../../config';
-import { setCookie, getCookie } from '../../../utils/cookies';
+import useCourseCardData from './hooks';
import './index.scss';
export const CourseCard = ({ course, isPersonalizedRecommendation }) => {
const isCollapsed = useIsCollapsed();
-
- const handleCourseClick = () => {
- const cookieName = configuration.PERSONALIZED_RECOMMENDATION_COOKIE_NAME;
- let recommendedCourses = getCookie(cookieName);
- if (typeof recommendedCourses === 'undefined') {
- recommendedCourses = { course_keys: [course.courseKey] };
- } else if (!recommendedCourses.course_keys.includes(course.courseKey)) {
- recommendedCourses.course_keys.push(course.courseKey);
- }
- recommendedCourses.is_personalized_recommendation = isPersonalizedRecommendation;
- setCookie(cookieName, JSON.stringify(recommendedCourses), 365);
- };
+ const { handleCourseClick } = useCourseCardData(course, isPersonalizedRecommendation);
return (
-
+
{
+ const handleCourseClick = (e) => {
+ e.preventDefault();
+
+ const cookieName = configuration.PERSONALIZED_RECOMMENDATION_COOKIE_NAME;
+ let recommendedCourses = getCookie(cookieName);
+ if (typeof recommendedCourses === 'undefined') {
+ recommendedCourses = { course_keys: [course.courseKey] };
+ } else if (!recommendedCourses.course_keys.includes(course.courseKey)) {
+ recommendedCourses.course_keys.push(course.courseKey);
+ }
+ recommendedCourses.is_personalized_recommendation = isPersonalized;
+ setCookie(cookieName, JSON.stringify(recommendedCourses), 365);
+
+ track.recommendedCourseClicked(
+ course.courseKey,
+ isPersonalized,
+ course?.marketingUrl,
+ );
+ };
+ return { handleCourseClick };
+};
+
+export default useCourseCardData;
diff --git a/src/widgets/RecommendationsPanel/hooks.js b/src/widgets/RecommendationsPanel/hooks.js
index 86bb1ba..9918cb2 100644
--- a/src/widgets/RecommendationsPanel/hooks.js
+++ b/src/widgets/RecommendationsPanel/hooks.js
@@ -2,7 +2,6 @@ import React from 'react';
import { StrictDict } from 'utils';
import { RequestStates } from 'data/constants/requests';
-import { handleEvent } from 'data/services/segment/utils';
import * as module from './hooks';
import api from './api';
@@ -38,19 +37,13 @@ export const useRecommendationPanelData = () => {
module.useFetchCourses(setRequestState, setData);
const courses = data.data?.courses || [];
const isPersonalizedRecommendation = data.data?.isPersonalizedRecommendation || false;
- const courseSearchClickTracker = () => handleEvent(searchCourseEventName, {
- pageName: 'learner_home',
- linkType: 'button',
- linkCategory: 'search_button',
- });
return {
courses,
- isPersonalizedRecommendation,
isLoaded: requestState === RequestStates.completed && courses.length > 0,
isFailed: requestState === RequestStates.failed
|| (requestState === RequestStates.completed && courses.length === 0),
isLoading: requestState === RequestStates.pending,
- courseSearchClickTracker,
+ isPersonalizedRecommendation,
};
};
diff --git a/src/widgets/RecommendationsPanel/hooks.test.js b/src/widgets/RecommendationsPanel/hooks.test.js
index 256a52f..7d7811a 100644
--- a/src/widgets/RecommendationsPanel/hooks.test.js
+++ b/src/widgets/RecommendationsPanel/hooks.test.js
@@ -2,7 +2,6 @@ import React from 'react';
import { MockUseState } from 'testUtils';
import { RequestStates } from 'data/constants/requests';
-import { handleEvent } from 'data/services/segment/utils';
import api from './api';
import * as hooks from './hooks';
@@ -10,9 +9,6 @@ import * as hooks from './hooks';
jest.mock('./api', () => ({
fetchRecommendedCourses: jest.fn(),
}));
-jest.mock('data/services/segment/utils', () => ({
- handleEvent: jest.fn(),
-}));
const state = new MockUseState(hooks);
@@ -100,16 +96,6 @@ describe('RecommendationsPanel hooks', () => {
it('initializes requestState as RequestStates.pending', () => {
state.expectInitializedWith(state.keys.requestState, RequestStates.pending);
});
- describe('courseSearchClickTracker behavior', () => {
- it('calls handleEvent with correct args', () => {
- out.courseSearchClickTracker();
- expect(handleEvent).toHaveBeenCalledWith(hooks.searchCourseEventName, {
- pageName: 'learner_home',
- linkType: 'button',
- linkCategory: 'search_button',
- });
- });
- });
describe('output', () => {
describe('request is completed, with returned courses', () => {
beforeEach(() => {
@@ -130,6 +116,19 @@ describe('RecommendationsPanel hooks', () => {
expect(out.courses).toEqual(testList);
});
});
+ describe('personalize recommendation', () => {
+ it('default to false', () => {
+ state.mockVal(state.keys.data, {});
+ out = hooks.useRecommendationPanelData();
+ expect(out.isPersonalizedRecommendation).toEqual(false);
+ });
+ it('is based on data', () => {
+ const expectOutput = { test: 'abirary' };
+ state.mockVal(state.keys.data, { data: { isPersonalizedRecommendation: expectOutput } });
+ out = hooks.useRecommendationPanelData();
+ expect(out.isPersonalizedRecommendation).toEqual(expectOutput);
+ });
+ });
describe('request is completed, with no returned courses', () => {
beforeEach(() => {
state.mockVal(state.keys.requestState, RequestStates.completed);
diff --git a/src/widgets/RecommendationsPanel/index.jsx b/src/widgets/RecommendationsPanel/index.jsx
index 27ea083..54849af 100644
--- a/src/widgets/RecommendationsPanel/index.jsx
+++ b/src/widgets/RecommendationsPanel/index.jsx
@@ -8,11 +8,10 @@ import hooks from './hooks';
export const RecommendationsPanel = () => {
const {
courses,
- isPersonalizedRecommendation,
isFailed,
isLoaded,
isLoading,
- courseSearchClickTracker,
+ isPersonalizedRecommendation,
} = hooks.useRecommendationPanelData();
if (isLoading) {
@@ -20,18 +19,14 @@ export const RecommendationsPanel = () => {
}
if (isLoaded) {
return (
-
+
);
}
if (isFailed) {
- return ();
+ return ();
}
// default fallback
- return ();
+ return ();
};
export default RecommendationsPanel;
diff --git a/src/widgets/RecommendationsPanel/index.test.jsx b/src/widgets/RecommendationsPanel/index.test.jsx
index b80f8fc..e72c34f 100644
--- a/src/widgets/RecommendationsPanel/index.test.jsx
+++ b/src/widgets/RecommendationsPanel/index.test.jsx
@@ -18,16 +18,15 @@ jest.mock('./LoadedView', () => 'LoadedView');
const { courses } = mockData;
describe('RecommendationsPanel snapshot', () => {
- const defaultProps = {
- courseSearchClickTracker: jest.fn().mockName('courseSearchClickTracker'),
+ const defaultLoadedViewProps = {
+ courses: [],
+ isPersonalizedRecommendation: false,
};
const defaultValues = {
isFailed: false,
isLoaded: false,
isLoading: false,
- courses: [],
- isPersonalizedRecommendation: false,
- ...defaultProps,
+ ...defaultLoadedViewProps,
};
it('displays LoadingView if request is loading', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
@@ -43,13 +42,7 @@ describe('RecommendationsPanel snapshot', () => {
isLoaded: true,
});
expect(shallow()).toMatchObject(
- shallow(
- ,
- ),
+ shallow(),
);
});
it('displays LookingForChallengeWidget if request is failed', () => {
@@ -58,7 +51,7 @@ describe('RecommendationsPanel snapshot', () => {
isFailed: true,
});
expect(shallow()).toMatchObject(
- shallow(),
+ shallow(),
);
});
it('defaults to LookingForChallengeWidget if no flags are true', () => {
@@ -66,7 +59,7 @@ describe('RecommendationsPanel snapshot', () => {
...defaultValues,
});
expect(shallow()).toMatchObject(
- shallow(),
+ shallow(),
);
});
});
diff --git a/src/widgets/RecommendationsPanel/messages.js b/src/widgets/RecommendationsPanel/messages.js
index aaae03e..69bedd4 100644
--- a/src/widgets/RecommendationsPanel/messages.js
+++ b/src/widgets/RecommendationsPanel/messages.js
@@ -4,12 +4,12 @@ const messages = defineMessages({
recommendationsHeading: {
id: 'RecommendationsPanel.recommendationsHeading',
defaultMessage: 'Recommendations for you',
- description: 'Heading on recommendations panel with personalized recommendations',
+ description: 'Personalize courses heading on recommendations panel',
},
popularCoursesHeading: {
id: 'RecommendationsPanel.popularCoursesHeading',
- defaultMessage: 'Popular on edX',
- description: 'Heading on recommendations panel with general recommendations',
+ defaultMessage: 'Popular courses',
+ description: 'Popular courses heading on recommendations panel',
},
exploreCoursesButton: {
id: 'RecommendationsPanel.exploreCoursesButton',
diff --git a/src/widgets/RecommendationsPanel/track.js b/src/widgets/RecommendationsPanel/track.js
new file mode 100644
index 0000000..a27c4d4
--- /dev/null
+++ b/src/widgets/RecommendationsPanel/track.js
@@ -0,0 +1,28 @@
+import { createLinkTracker, createEventTracker } from 'data/services/segment/utils';
+
+export const eventNames = {
+ findCoursesClicked: 'edx.bi.dashboard.find_courses_button.clicked',
+ recommendedCourseClicked: 'edx.bi.user.recommended.course.click',
+};
+
+export const findCoursesClicked = (href) => createLinkTracker(
+ createEventTracker(eventNames.findCoursesClicked, {
+ pageName: 'learner_home',
+ linkType: 'button',
+ linkCategory: 'search_button',
+ }),
+ href,
+);
+
+export const recommendedCourseClicked = (courseKey, isPersonalized, href) => createLinkTracker(
+ createEventTracker(eventNames.recommendedCoursesClicked, {
+ course_key: courseKey,
+ is_personalized_recommendation: isPersonalized,
+ }),
+ href,
+);
+
+export default {
+ findCoursesClicked,
+ recommendedCourseClicked,
+};