diff --git a/src/containers/Dashboard/index.jsx b/src/containers/Dashboard/index.jsx
index bab2361..bb31684 100644
--- a/src/containers/Dashboard/index.jsx
+++ b/src/containers/Dashboard/index.jsx
@@ -13,7 +13,7 @@ import LoadingView from './LoadingView';
import DashboardLayout from './DashboardLayout';
import hooks from './hooks';
import './index.scss';
-import ProductRecommendationsContainer from '../../widgets/ProductRecommendations/components/ProductRecommendationsContainer';
+import ProductRecommendations from '../../widgets/ProductRecommendations';
export const Dashboard = () => {
hooks.useInitializeDashboard();
@@ -22,6 +22,8 @@ export const Dashboard = () => {
const hasAvailableDashboards = reduxHooks.useHasAvailableDashboards();
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
+ const shouldShowProductRecommendations = !initIsPending && !hasAvailableDashboards && hasCourses && false;
+
return (
- {courses
- && courseTypes.map((type) => (
-
-
-
- {courses
- .filter((course) => course.courseType === type)
- .map((item) => (
-
- ))}
-
+const ProductCardContainer = ({ finalProductList, courseTypes }) => (
+
+ {finalProductList
+ && courseTypes.map((type) => (
+
+
+
+ {finalProductList
+ .filter((course) => courseTypeToProductTypeMap[course.courseType] === type)
+ .map((item) => (
+
+ ))}
- ))}
-
- );
-};
+
+ ))}
+
+);
ProductCardContainer.propTypes = {
- courses: PropTypes.arrayOf(
- PropTypes.shape({
- key: PropTypes.string,
- uuid: PropTypes.string,
- title: PropTypes.string,
- image: PropTypes.shape({
- src: PropTypes.string,
- }),
- prospectusPath: PropTypes.string,
- owners: PropTypes.arrayOf(
- PropTypes.shape({
- key: PropTypes.string,
- name: PropTypes.string,
- logoImageUrl: PropTypes.string,
- }),
- ),
- activeCourseRun: PropTypes.shape({
- key: PropTypes.string,
- marketingUrl: PropTypes.string,
- }),
- courseType: PropTypes.string,
- }),
+ finalProductList: PropTypes.arrayOf(
+ PropTypes.shape(courseShape),
).isRequired,
+ courseTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
};
export default ProductCardContainer;
diff --git a/src/widgets/ProductRecommendations/components/ProductCardContainer.test.jsx b/src/widgets/ProductRecommendations/components/ProductCardContainer.test.jsx
new file mode 100644
index 0000000..c737461
--- /dev/null
+++ b/src/widgets/ProductRecommendations/components/ProductCardContainer.test.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { mockCrossProductCourses, mockOpenCourses } from '../testData';
+import ProductCardContainer from './ProductCardContainer';
+
+describe('ProductRecommendations ProductCardContainer', () => {
+ const props = {
+ finalProductList: [...mockCrossProductCourses, ...mockOpenCourses],
+ courseTypes: ['Executive Education', 'Boot Camp', 'Course'],
+ };
+
+ it('matches snapshot', () => {
+ expect(shallow(
)).toMatchSnapshot();
+ });
+
+ describe('with finalCourseList containing cross product and open courses', () => {
+ it('renders 3 ProductCardHeaders with the 3 different course types', () => {
+ const wrapper = shallow(
);
+ const productCardHeaders = wrapper.find('ProductCardHeader');
+
+ expect(productCardHeaders.length).toEqual(3);
+ productCardHeaders.forEach((header, index) => {
+ expect(header.props().courseType).toEqual(props.courseTypes[index]);
+ });
+ });
+ });
+
+ describe('with finalCourseList containing only open courses', () => {
+ it('renders 1 ProductHeader with the one course type', () => {
+ const openCoursesProps = {
+ finalProductList: [...mockOpenCourses, ...mockOpenCourses],
+ courseTypes: ['Course'],
+ };
+
+ const wrapper = shallow(
);
+ const productCardHeaders = wrapper.find('ProductCardHeader');
+
+ expect(productCardHeaders.length).toEqual(1);
+ expect(productCardHeaders.at(0).props().courseType).toEqual(openCoursesProps.courseTypes[0]);
+ });
+ });
+});
diff --git a/src/widgets/ProductRecommendations/components/ProductCardHeader.jsx b/src/widgets/ProductRecommendations/components/ProductCardHeader.jsx
index 741aae1..21e204a 100644
--- a/src/widgets/ProductRecommendations/components/ProductCardHeader.jsx
+++ b/src/widgets/ProductRecommendations/components/ProductCardHeader.jsx
@@ -12,14 +12,13 @@ function ProductCardHeader({ courseType }) {
const getProductTypeDetail = (type) => {
switch (type) {
- case 'executive-education':
- case 'executive-education-2u':
+ case 'Executive Education':
return {
heading: messages.executiveEducationHeading,
description: messages.executiveEducationDescription,
url: '/executive-education',
};
- case 'bootcamp-2u':
+ case 'Boot Camp':
return {
heading: messages.bootcampHeading,
description: messages.bootcampDescription,
diff --git a/src/widgets/ProductRecommendations/components/ProductCardHeader.test.jsx b/src/widgets/ProductRecommendations/components/ProductCardHeader.test.jsx
new file mode 100644
index 0000000..5fc7907
--- /dev/null
+++ b/src/widgets/ProductRecommendations/components/ProductCardHeader.test.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { mockCrossProductCourses } from '../testData';
+import ProductCardHeader from './ProductCardHeader';
+import { courseTypeToProductTypeMap } from '../utils';
+
+describe('ProductRecommendations ProductCardHeader', () => {
+ const course = mockCrossProductCourses[0];
+
+ it('matches snapshot', () => {
+ expect(shallow(
)).toMatchSnapshot();
+ });
+});
diff --git a/src/widgets/ProductRecommendations/components/ProductRecommendationsContainer.jsx b/src/widgets/ProductRecommendations/components/ProductRecommendationsContainer.jsx
deleted file mode 100644
index 493d008..0000000
--- a/src/widgets/ProductRecommendations/components/ProductRecommendationsContainer.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import { Container } from '@edx/paragon';
-import { useIntl } from '@edx/frontend-platform/i18n';
-
-import './index.scss';
-import messages from '../messages';
-import useCrossProductRecommendationsData from '../hooks';
-import ProductCardContainer from './ProductCardContainer';
-import mockCrossProductRecommendations from '../mockData';
-
-const ProductRecommendationsContainer = () => {
- const { formatMessage } = useIntl();
- const mockRecommendations = mockCrossProductRecommendations.courses;
- const { courses, isLoading } = useCrossProductRecommendationsData();
-
- return (
-
- {!isLoading && (
-
-
- {formatMessage(messages.recommendationsHeading)}
-
-
-
- )}
-
- );
-};
-
-export default ProductRecommendationsContainer;
diff --git a/src/widgets/ProductRecommendations/components/__snapshots__/LoadedView.test.jsx.snap b/src/widgets/ProductRecommendations/components/__snapshots__/LoadedView.test.jsx.snap
new file mode 100644
index 0000000..e1acc21
--- /dev/null
+++ b/src/widgets/ProductRecommendations/components/__snapshots__/LoadedView.test.jsx.snap
@@ -0,0 +1,89 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProductRecommendations LoadedView matches snapshot 1`] = `
+
+
+
+ You might also like
+
+
+
+
+`;
diff --git a/src/widgets/ProductRecommendations/components/__snapshots__/LoadingView.test.jsx.snap b/src/widgets/ProductRecommendations/components/__snapshots__/LoadingView.test.jsx.snap
new file mode 100644
index 0000000..1203443
--- /dev/null
+++ b/src/widgets/ProductRecommendations/components/__snapshots__/LoadingView.test.jsx.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProductRecommendations LoadingView matches snapshot 1`] = `
+
+`;
diff --git a/src/widgets/ProductRecommendations/components/__snapshots__/ProductCard.test.jsx.snap b/src/widgets/ProductRecommendations/components/__snapshots__/ProductCard.test.jsx.snap
new file mode 100644
index 0000000..7138134
--- /dev/null
+++ b/src/widgets/ProductRecommendations/components/__snapshots__/ProductCard.test.jsx.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProductRecommendations ProductCard matches snapshot 1`] = `
+
+
+
+
+
+ Harvard University
+
+ }
+ title={
+
+ Introduction to Computer Science
+
+ }
+ />
+
+
+
+ Executive Education
+
+
+
+
+
+
+`;
diff --git a/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardContainer.test.jsx.snap b/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardContainer.test.jsx.snap
new file mode 100644
index 0000000..52cb8cf
--- /dev/null
+++ b/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardContainer.test.jsx.snap
@@ -0,0 +1,95 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProductRecommendations ProductCardContainer matches snapshot 1`] = `
+
+`;
diff --git a/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardHeader.test.jsx.snap b/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardHeader.test.jsx.snap
new file mode 100644
index 0000000..5dd50f6
--- /dev/null
+++ b/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardHeader.test.jsx.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProductRecommendations ProductCardHeader matches snapshot 1`] = `
+
+
+
+
+ Executive Education
+
+
+
+
+
+ Short Courses to develop leadership skills
+
+
+`;
diff --git a/src/widgets/ProductRecommendations/hooks.js b/src/widgets/ProductRecommendations/hooks.js
index d9ac808..d9bf203 100644
--- a/src/widgets/ProductRecommendations/hooks.js
+++ b/src/widgets/ProductRecommendations/hooks.js
@@ -1,61 +1,65 @@
-import { useState, useEffect } from "react";
-import { StrictDict } from 'utils';
+import { useState, useEffect } from 'react';
import { RequestStates } from 'data/constants/requests';
+import { StrictDict } from 'utils';
+import { reduxHooks } from 'hooks';
+import { SortKeys } from 'data/constants/app';
import api from './api';
import * as module from './hooks';
export const state = StrictDict({
requestState: (val) => useState(val), // eslint-disable-line
- courses: (val) => useState(val), // eslint-disable-line
+ data: (val) => useState(val), // eslint-disable-line
});
-const useFetchMostRecentCourse = () => {
- // Gets the most recent course a user is enrolled in
- // Goes through the courses enrollment property and checks the lastEnrolled property?
- // Can we assume that the order the courses are listed on the Dashbaord is the order that the course was enrolled in?
+export const useMostRecentCourseRunKey = () => {
+ const mostRecentCourse = reduxHooks.useCurrentCourseList({
+ sortBy: SortKeys.enrolled,
+ filters: [],
+ pageSize: 0,
+ }).visible[0].courseRun.courseId;
+
+ return mostRecentCourse;
};
-const useFetchCrossProductCourses = (setRequestState, setCourses) => {
+export const useFetchProductRecommendations = (setRequestState, setData) => {
+ const courseRunKey = module.useMostRecentCourseRunKey();
useEffect(() => {
let isMounted = true;
- if (isMounted) {
- setTimeout(() => {
- console.log("timing")
- api
- .fetchCrossProductCourses('course-v1:IBM+IBMBCC001+1T2022')
- .then((response) => {
- console.log("Here is the response", response)
- if (response.status === 200) {
- setRequestState(RequestStates.completed);
- setCourses(response.data.courses);
- }
- }).catch(err => {
- console.log("here is the error", err);
- });
- }, 5000);
-
- }
- return () => { isMounted = false; };
- /* eslint-disable */
- }, []); // most recent course ID will be the dependancy;
+ api
+ .fetchProductRecommendations(courseRunKey)
+ .then((response) => {
+ if (isMounted) {
+ setData(response.data);
+ setRequestState(RequestStates.completed);
+ }
+ })
+ .catch(() => {
+ if (isMounted) {
+ setRequestState(RequestStates.failed);
+ }
+ });
+ return () => {
+ isMounted = false;
+ };
+ /* eslint-disable */
+ }, []);
};
-const useFetchAlgoliaRecommendations = () => {
- // This hook will fetch algolia reccs and return the reccs
-}
-
-const useCrossProductRecommendationsData = () => {
- const [requestState, setRequestState] = state.requestState(RequestStates.pending);
- const [courses, setCourses] = state.courses([]);
- useFetchCrossProductCourses(setRequestState, setCourses);
+export const useProductRecommendationsData = () => {
+ const [requestState, setRequestState] = module.state.requestState(
+ RequestStates.pending
+ );
+ const [data, setData] = module.state.data({});
+ module.useFetchProductRecommendations(setRequestState, setData);
return {
- courses,
- isLoading: requestState === RequestStates.pending
- }
+ productRecommendations: data,
+ isLoading: requestState === RequestStates.pending,
+ isLoaded: requestState === RequestStates.completed,
+ hasFailed: requestState === RequestStates.failed
+ };
+};
-}
-
-export default useCrossProductRecommendationsData;
+export default { useProductRecommendationsData };
diff --git a/src/widgets/ProductRecommendations/hooks.test.js b/src/widgets/ProductRecommendations/hooks.test.js
new file mode 100644
index 0000000..639a08b
--- /dev/null
+++ b/src/widgets/ProductRecommendations/hooks.test.js
@@ -0,0 +1,190 @@
+import React from 'react';
+
+import { MockUseState } from 'testUtils';
+import { RequestStates } from 'data/constants/requests';
+import { reduxHooks } from 'hooks';
+
+import api from './api';
+import * as hooks from './hooks';
+
+jest.mock('./api', () => ({
+ fetchProductRecommendations: jest.fn(),
+}));
+
+jest.mock('hooks', () => ({
+ reduxHooks: {
+ useCurrentCourseList: jest.fn(),
+ },
+}));
+
+const state = new MockUseState(hooks);
+const mostRecentCourseRunKey = 'course ID 1';
+
+const courses = [
+ {
+ courseRun: {
+ courseId: mostRecentCourseRunKey,
+ },
+ },
+ {
+ courseRun: {
+ courseId: 'course ID 2',
+ },
+ },
+];
+
+const courseListData = {
+ visible: courses,
+ numPages: 0,
+};
+
+let output;
+describe('ProductRecommendations hooks', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('state fields', () => {
+ state.testGetter(state.keys.requestState);
+ state.testGetter(state.keys.data);
+ });
+
+ describe('useMostRecentCourseRunKey', () => {
+ it('returns the courseId of the first course in the sorted visible array', () => {
+ reduxHooks.useCurrentCourseList.mockReturnValueOnce(courseListData);
+
+ expect(hooks.useMostRecentCourseRunKey()).toBe(mostRecentCourseRunKey);
+ });
+ });
+
+ describe('useFetchProductRecommendations', () => {
+ describe('behavior', () => {
+ describe('useEffect call', () => {
+ let calls;
+ let cb;
+ const response = { data: 'response data' };
+ const setRequestState = jest.fn();
+ const setData = jest.fn();
+ beforeEach(() => {
+ reduxHooks.useCurrentCourseList.mockReturnValue(courseListData);
+ hooks.useFetchProductRecommendations(setRequestState, setData);
+ ({ calls } = React.useEffect.mock);
+ ([[cb]] = calls);
+ });
+ it('calls useEffect once', () => {
+ expect(calls.length).toEqual(1);
+ });
+ it('calls fetchProductRecommendations with the most recently enrolled courseId', () => {
+ api.fetchProductRecommendations.mockReturnValueOnce(Promise.resolve(response));
+ cb();
+ expect(api.fetchProductRecommendations).toHaveBeenCalledWith(mostRecentCourseRunKey);
+ });
+ describe('successful fetch on mounted component', () => {
+ it('sets request state to completed and loads api response', async () => {
+ let resolveFn;
+ api.fetchProductRecommendations.mockReturnValueOnce(new Promise(resolve => {
+ resolveFn = resolve;
+ }));
+ cb();
+ expect(api.fetchProductRecommendations).toHaveBeenCalledWith(mostRecentCourseRunKey);
+ expect(setRequestState).not.toHaveBeenCalled();
+ expect(setData).not.toHaveBeenCalledWith(response.data);
+ await resolveFn(response);
+ expect(setRequestState).toHaveBeenCalledWith(RequestStates.completed);
+ expect(setData).toHaveBeenCalledWith(response.data);
+ });
+ });
+ describe('successful fetch on unmounted component', () => {
+ it('it does not set the state', async () => {
+ let resolveFn;
+ api.fetchProductRecommendations.mockReturnValueOnce(new Promise(resolve => {
+ resolveFn = resolve;
+ }));
+ const unMount = cb();
+ expect(api.fetchProductRecommendations).toHaveBeenCalledWith(mostRecentCourseRunKey);
+ expect(setRequestState).not.toHaveBeenCalled();
+ expect(setData).not.toHaveBeenCalledWith(response.data);
+ unMount();
+ await resolveFn(response);
+ expect(setRequestState).not.toHaveBeenCalled();
+ expect(setData).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+ });
+ describe('useProductRecommendationsData', () => {
+ let fetchSpy;
+ beforeEach(() => {
+ state.mock();
+ fetchSpy = jest.spyOn(hooks, 'useFetchProductRecommendations').mockImplementationOnce(() => {});
+ output = hooks.useProductRecommendationsData();
+ });
+ it('calls useFetchProductRecommendations with setRequestState and setData', () => {
+ expect(fetchSpy).toHaveBeenCalledWith(state.setState.requestState, state.setState.data);
+ });
+ it('initializes requestState as RequestStates.pending', () => {
+ state.expectInitializedWith(state.keys.requestState, RequestStates.pending);
+ });
+ describe('return values', () => {
+ describe('request is completed, with returned response object', () => {
+ const mockResponse = { crossProductCourses: {}, amplitudeCourses: {} };
+ beforeEach(() => {
+ state.mockVal(state.keys.requestState, RequestStates.completed);
+ state.mockVal(state.keys.data, mockResponse);
+ output = hooks.useProductRecommendationsData();
+ });
+ it('is not loading', () => {
+ expect(output.isLoading).toEqual(false);
+ });
+ it('is loaded', () => {
+ expect(output.isLoaded).toEqual(true);
+ });
+ it('has not failed', () => {
+ expect(output.hasFailed).toEqual(false);
+ });
+ it('returns country code', () => {
+ expect(output.productRecommendations).toEqual(mockResponse);
+ });
+ });
+ describe('request is pending', () => {
+ beforeEach(() => {
+ state.mockVal(state.keys.requestState, RequestStates.pending);
+ state.mockVal(state.keys.data, {});
+ output = hooks.useProductRecommendationsData();
+ });
+ it('is loading', () => {
+ expect(output.isLoading).toEqual(true);
+ });
+ it('is not loaded', () => {
+ expect(output.isLoaded).toEqual(false);
+ });
+ it('has not failed', () => {
+ expect(output.hasFailed).toEqual(false);
+ });
+ it('returns empty object', () => {
+ expect(output.productRecommendations).toEqual({});
+ });
+ });
+ describe('request has failed', () => {
+ beforeEach(() => {
+ state.mockVal(state.keys.requestState, RequestStates.failed);
+ state.mockVal(state.keys.data, {});
+ output = hooks.useProductRecommendationsData();
+ });
+ it('is not loading', () => {
+ expect(output.isLoading).toEqual(false);
+ });
+ it('is not loaded', () => {
+ expect(output.isLoaded).toEqual(false);
+ });
+ it('has failed', () => {
+ expect(output.hasFailed).toEqual(true);
+ });
+ it('returns empty object', () => {
+ expect(output.productRecommendations).toEqual({});
+ });
+ });
+ });
+ });
+});
diff --git a/src/widgets/ProductRecommendations/index.jsx b/src/widgets/ProductRecommendations/index.jsx
index e69de29..b830225 100644
--- a/src/widgets/ProductRecommendations/index.jsx
+++ b/src/widgets/ProductRecommendations/index.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import './index.scss';
+import { useWindowSize, breakpoints } from '@edx/paragon';
+import { useProductRecommendationsData } from './hooks';
+import LoadingView from './components/LoadingView';
+import LoadedView from './components/LoadedView';
+
+const ProductRecommendations = () => {
+ const {
+ productRecommendations,
+ isLoading,
+ isLoaded,
+ hasFailed,
+ } = useProductRecommendationsData();
+
+ const { width } = useWindowSize();
+ const isMobile = width < breakpoints.small.minWidth;
+
+ if (isLoading && !isMobile && !hasFailed) {
+ return
;
+ }
+
+ if (isLoaded && !isMobile && !hasFailed) {
+ return (
+
+ );
+ }
+
+ return null;
+};
+
+export default ProductRecommendations;
diff --git a/src/widgets/ProductRecommendations/components/index.scss b/src/widgets/ProductRecommendations/index.scss
similarity index 53%
rename from src/widgets/ProductRecommendations/components/index.scss
rename to src/widgets/ProductRecommendations/index.scss
index d5cb491..5094d46 100644
--- a/src/widgets/ProductRecommendations/components/index.scss
+++ b/src/widgets/ProductRecommendations/index.scss
@@ -2,24 +2,21 @@
$horizontal-card-gap: 20px;
$vertical-card-gap: 24px;
-$card-height: 332px;
-$header-height: 104px;
-$card-width: 270px;
-.recommendations-container {
- border: solid;
-}
+// .recommendations-container {
+// border: solid;
+// }
.base-card {
- height: $card-height;
- width: $card-width !important;
+ height: 332px;
+ width: 270px !important;
p {
margin-bottom: 0;
}
.pgn__card-image-cap {
- height: $header-height;
+ height: 104px;
object: {
fit: cover;
position: top center;
@@ -83,33 +80,8 @@ $card-width: 270px;
}
}
- &.dark {
- background-color: $primary-500;
-
- .pgn__card-header-title-md {
- color: $white;
- }
-
- .pgn__card-header-subtitle-md {
- color: $light-200;
- }
-
- .title {
- color: $white;
- }
-
- .subtitle {
- color: $light-200;
- }
-
- .badge {
- background-color: $dark-200;
- color: $white;
- }
-
- .footer-content {
- color: $light-200;
- }
+ &:hover {
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15), 0 0.125rem 0.5rem rgba(0, 0, 0, 0.15);
}
}
@@ -117,10 +89,6 @@ $card-width: 270px;
text-decoration: none;
}
-.base-card:hover {
- box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15), 0 0.125rem 0.5rem rgba(0, 0, 0, 0.15);
-}
-
.base-card-link .base-card {
display: flex;
}
@@ -128,26 +96,14 @@ $card-width: 270px;
.product-card-container {
gap: $vertical-card-gap $horizontal-card-gap;
margin: 0 (-$horizontal-card-gap);
- padding: 0 $horizontal-card-gap;
+ padding: 1rem $horizontal-card-gap;
.course-subcontainer {
display: flex;
gap: $vertical-card-gap $horizontal-card-gap;
}
- // .base-card-wrapper {
- // flex: 0 1 calc(100% - #{$horizontal-card-gap});
-
- // @include media-breakpoint-up(sm) {
- // flex: 0 1 calc(50% - #{$horizontal-card-gap});
- // }
-
- // @include media-breakpoint-up(md) {
- // flex: 0 1 calc(33.333% - #{$horizontal-card-gap});
- // }
-
- // @include media-breakpoint-up(xl) {
- // flex: 0 1 calc(25% - #{$horizontal-card-gap});
- // }
- // }
-}
\ No newline at end of file
+ @include media-breakpoint-down(lg) {
+ overflow-x: scroll;
+ }
+}
diff --git a/src/widgets/ProductRecommendations/index.test.jsx b/src/widgets/ProductRecommendations/index.test.jsx
new file mode 100644
index 0000000..c100cc4
--- /dev/null
+++ b/src/widgets/ProductRecommendations/index.test.jsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { useWindowSize } from '@edx/paragon';
+import hooks from './hooks';
+import ProductRecommendations from './index';
+import LoadingView from './components/LoadingView';
+import LoadedView from './components/LoadedView';
+import { mockResponse } from './testData';
+
+jest.mock('./hooks', () => ({
+ useProductRecommendationsData: jest.fn(),
+}));
+jest.mock('./components/LoadingView', () => 'LoadingView');
+jest.mock('./components/LoadedView', () => 'LoadedView');
+
+describe('ProductRecommendations', () => {
+ const defaultValues = {
+ productRecommendations: {},
+ isLoading: false,
+ isLoaded: false,
+ hasFailed: false,
+ };
+
+ const successfullLoadValues = {
+ ...defaultValues,
+ isLoaded: true,
+ productRecommendations: mockResponse,
+ };
+
+ const desktopWindowSize = {
+ width: 1400,
+ height: 943,
+ };
+
+ it('matches snapshot', () => {
+ useWindowSize.mockReturnValueOnce(desktopWindowSize);
+ hooks.useProductRecommendationsData.mockReturnValueOnce({
+ ...successfullLoadValues,
+ });
+
+ expect(shallow(
)).toMatchSnapshot();
+ });
+ it('renders the LoadedView with course data if the request completed', () => {
+ useWindowSize.mockReturnValueOnce(desktopWindowSize);
+ hooks.useProductRecommendationsData.mockReturnValueOnce({
+ ...successfullLoadValues,
+ });
+
+ expect(shallow(
)).toMatchObject(
+ shallow(
+
,
+ ),
+ );
+ });
+ it('renders the LoadingView if the request is pending', () => {
+ useWindowSize.mockReturnValueOnce(desktopWindowSize);
+ hooks.useProductRecommendationsData.mockReturnValueOnce({
+ ...defaultValues,
+ isLoading: true,
+ });
+
+ expect(shallow(
)).toMatchObject(
+ shallow(
),
+ );
+ });
+ it('renders nothing if the request has failed', () => {
+ useWindowSize.mockReturnValueOnce(desktopWindowSize);
+ hooks.useProductRecommendationsData.mockReturnValueOnce({
+ ...defaultValues,
+ hasFailed: true,
+ });
+
+ const wrapper = shallow(
);
+
+ expect(wrapper.type()).toBeNull();
+ });
+ it('renders nothing if the width of the screen size is less than 576px (mobile view)', () => {
+ useWindowSize.mockReturnValueOnce({ width: 575, height: 976 });
+ hooks.useProductRecommendationsData.mockReturnValueOnce({
+ ...successfullLoadValues,
+ });
+
+ const wrapper = shallow(
);
+
+ expect(wrapper.type()).toBeNull();
+ });
+});
diff --git a/src/widgets/ProductRecommendations/mockData.js b/src/widgets/ProductRecommendations/mockData.js
deleted file mode 100644
index 550db97..0000000
--- a/src/widgets/ProductRecommendations/mockData.js
+++ /dev/null
@@ -1,100 +0,0 @@
-const mockCrossProductRecommendations = {
- courses: [
- {
- key: 'HarvardX+CRS',
- uuid: 'fee0ed87-a122-46a8-b233-3e5c75c755d1',
- title: 'CRISPR: Gene-editing Applications',
- image: {
- src: 'https://prod-discovery.edx-cdn.org/media/course/image/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg',
- },
- prospectusPath:
- 'course/harvard-vpal-crispr-gene-editing-applications-online-short-course',
- owners: [
- {
- key: 'HarvardX',
- name: 'Harvard University',
- logoImageUrl:
- 'http://localhost:18381/media/organization/logos/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png',
- },
- ],
- activeCourseRun: {
- key: 'course-v1:HarvardX+CRS+1T2023',
- marketingUrl:
- 'course/crispr-gene-editing-applications-course-v1-harvardx-crs-1t2023?utm_source=discovery_worker&utm_medium=affiliate_partner',
- },
- courseType: 'executive-education-2u',
- },
- {
- key: 'AdelaideX+BC24CYB',
- uuid: 'dacb53da-35c7-4c6b-b8d0-d7d8c0c78cef',
- title: 'Cybersecurity Boot Camp',
- image: {
- src: 'https://prod-discovery.edx-cdn.org/media/course/image/fbb62b99-9f85-4563-a67c-34ac827560bd-e3e6263b98fd.small.png',
- },
- prospectusPath:
- 'course/the-university-of-adelaide-cybersecurity-boot-camp',
- owners: [
- {
- key: 'AdelaideX',
- name: 'University of Adelaide',
- logoImageUrl:
- 'http://localhost:18381/media/organization/logos/51d054b4-9589-4376-9e9f-15656f3c8d0e-f10fb90c3ff1.png',
- },
- ],
- activeCourseRun: {
- key: 'course-v1:AdelaideX+BC24CYB+1T2023',
- marketingUrl:
- 'course/cybersecurity-boot-camp-course-v1-adelaidex-bc24cyb-1t2023?utm_source=discovery_worker&utm_medium=affiliate_partner',
- },
- courseType: 'bootcamp-2u',
- },
- {
- key: 'AA+AA101',
- uuid: '1e2cae8c-1c67-4067-a3c0-360543e6a9b8',
- title: 'Data Analytics for Business',
- image: {
- src: 'https://prod-discovery.edx-cdn.org/media/course/image/1e2cae8c-1c67-4067-a3c0-360543e6a9b8-50beebb61f2e.small.png',
- },
- prospectusPath: '/course/data-analytics-for-business',
- owners: [
- {
- key: 'GTx',
- name: 'The Georgia Institute of Technology',
- logoImageUrl:
- 'https://prod-discovery.edx-cdn.org/organization/logos/8537d31f-01b4-40fd-b652-e17b38eefe41-7956b2a3cd04.png',
- },
- ],
- activeCourseRun: {
- key: 'course-v1:GTx+MGT6203x+2T2023',
- marketingUrl:
- 'https://www.edx.org/course/data-analytics-for-business-course-v1gtxmgt6203x2t2023?utm_source=prospectus_worker&utm_medium=affiliate_partner',
- },
- courseType: 'course',
- },
- {
- key: 'AA+AA101',
- uuid: '876a65e7-425b-437b-bced-bdf8059fec81',
- title: 'Western and Chinese Art: Masters and Classics',
- image: {
- src: 'https://prod-discovery.edx-cdn.org/media/course/image/876a65e7-425b-437b-bced-bdf8059fec81.small.jpg',
- },
- prospectusPath: '/course/western-and-chinese-art-masters-and-classics',
- owners: [
- {
- key: 'TsinghuaX',
- name: 'Tsinghua University',
- logoImageUrl:
- 'https://prod-discovery.edx-cdn.org/organization/logos/b5714409-b5f4-4c9d-9348-b0fecbaaddd6-780fbb6c72c7.png',
- },
- ],
- activeCourseRun: {
- key: 'course-v1:TsinghuaX+00691153.x+1T2015',
- marketingUrl:
- 'https://www.edx.org/course/western-chinese-art-masters-classics-tsinghuax-00691153x?utm_source=prospectus_worker&utm_medium=affiliate_partner',
- },
- courseType: 'course',
- },
- ],
-};
-
-export default mockCrossProductRecommendations;
diff --git a/src/widgets/ProductRecommendations/testData.js b/src/widgets/ProductRecommendations/testData.js
new file mode 100644
index 0000000..8c904e8
--- /dev/null
+++ b/src/widgets/ProductRecommendations/testData.js
@@ -0,0 +1,31 @@
+const getCoursesWithType = (courseTypes) => {
+ const courses = [];
+
+ courseTypes.forEach((type) => {
+ courses.push({
+ title: 'Introduction to Computer Science',
+ image: {
+ src: 'https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg',
+ },
+ prospectusPath: 'course/introduction-to-computer-sceince',
+ owners: [
+ {
+ key: 'HarvardX',
+ name: 'Harvard University',
+ logoImageUrl: 'http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png',
+ },
+ ],
+ courseType: type,
+ });
+ });
+
+ return courses;
+};
+
+export const mockCrossProductCourses = getCoursesWithType(['executive-education-2u', 'bootcamp-2u']);
+export const mockOpenCourses = getCoursesWithType(['verified-audit', 'audit', 'verified', 'course']);
+
+export const mockResponse = {
+ crossProductCourses: mockCrossProductCourses,
+ amplitudeCourses: mockOpenCourses,
+};
diff --git a/src/widgets/ProductRecommendations/utils.js b/src/widgets/ProductRecommendations/utils.js
new file mode 100644
index 0000000..f2bd65a
--- /dev/null
+++ b/src/widgets/ProductRecommendations/utils.js
@@ -0,0 +1,42 @@
+import PropTypes from 'prop-types';
+
+export const courseShape = {
+ uuid: PropTypes.string,
+ title: PropTypes.string,
+ image: PropTypes.shape({
+ src: PropTypes.string,
+ }),
+ prospectusPath: PropTypes.string,
+ owners: PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.string,
+ name: PropTypes.string,
+ logoImageUrl: PropTypes.string,
+ }),
+ ),
+ activeCourseRun: PropTypes.shape({
+ key: PropTypes.string,
+ marketingUrl: PropTypes.string,
+ }),
+ courseType: PropTypes.string,
+};
+
+export const courseTypeToProductTypeMap = {
+ course: 'Course',
+ 'verified-audit': 'Course',
+ verified: 'Course',
+ audit: 'Course',
+ 'credit-verified-audit': 'Course',
+ 'spoc-verified-audit': 'Course',
+ professional: 'Professional Certificate',
+ 'bootcamp-2u': 'Boot Camp',
+ 'executive-education-2u': 'Executive Education',
+ 'executive-education': 'Executive Education',
+ masters: "Master's",
+ 'masters-verified-audit': "Master's",
+};
+
+export default {
+ courseShape,
+ courseTypeToProductTypeMap,
+};
diff --git a/src/widgets/RecommendationsPanel/index.jsx b/src/widgets/RecommendationsPanel/index.jsx
index b6c68b5..c091a8d 100644
--- a/src/widgets/RecommendationsPanel/index.jsx
+++ b/src/widgets/RecommendationsPanel/index.jsx
@@ -4,7 +4,6 @@ import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
import LoadingView from './LoadingView';
import LoadedView from './LoadedView';
import hooks from './hooks';
-import recommendedCoursesData from "../RecommendationsPanel/mockData";
export const RecommendationsPanel = () => {
const {
@@ -18,17 +17,14 @@ export const RecommendationsPanel = () => {
if (isLoading) {
return (
);
}
-
- const newCourses = recommendedCoursesData.courses;
-
- // if (newCourses.length > 0) {
- // return (
- //
- // );
- // }
- // if (isFailed) {
- // return (
);
- // }
+ if (isLoaded && courses.length > 0) {
+ return (
+
+ );
+ }
+ if (isFailed) {
+ return (
);
+ }
// default fallback
return (
);
};