From cb380a2031c1135ad60cf0ef1ba1f718e674a963 Mon Sep 17 00:00:00 2001
From: Blue
Date: Wed, 26 Jul 2023 10:49:47 +0500
Subject: [PATCH] feat: design change for recommendations (#997)
Description:
Design change for recommendations in which covered all the card type
VAN-1564
---
.../ProductCard/BaseCard/index.jsx | 80 ++++++
.../ProductCard/Footer/index.jsx | 126 +++++++++
src/recommendations/ProductCard/index.jsx | 129 +++++++++
src/recommendations/RecommendationCard.jsx | 87 ------
src/recommendations/RecommendationsList.jsx | 41 ++-
src/recommendations/RecommendationsPage.jsx | 69 +----
src/recommendations/data/constants.js | 7 +
src/recommendations/data/tests/utils.test.jsx | 16 ++
src/recommendations/data/utils.js | 55 +++-
src/recommendations/messages.js | 107 ++++++-
.../tests/RecommendationsList.test.jsx | 140 +++++++++
.../tests/RecommendationsPage.test.jsx | 101 ++-----
src/recommendations/tests/mockedData.js | 266 ++++++++++++++----
src/sass/_recommendations_card_base.scss | 133 +++++++++
src/sass/_recommendations_page.scss | 24 ++
src/sass/_style.scss | 1 +
16 files changed, 1095 insertions(+), 287 deletions(-)
create mode 100644 src/recommendations/ProductCard/BaseCard/index.jsx
create mode 100644 src/recommendations/ProductCard/Footer/index.jsx
create mode 100644 src/recommendations/ProductCard/index.jsx
delete mode 100644 src/recommendations/RecommendationCard.jsx
create mode 100644 src/recommendations/data/tests/utils.test.jsx
create mode 100644 src/recommendations/tests/RecommendationsList.test.jsx
create mode 100644 src/sass/_recommendations_card_base.scss
diff --git a/src/recommendations/ProductCard/BaseCard/index.jsx b/src/recommendations/ProductCard/BaseCard/index.jsx
new file mode 100644
index 00000000..3d20fe89
--- /dev/null
+++ b/src/recommendations/ProductCard/BaseCard/index.jsx
@@ -0,0 +1,80 @@
+import React from 'react';
+
+import { Badge, Card, Hyperlink } from '@edx/paragon';
+import PropTypes from 'prop-types';
+
+import { truncateText } from '../../data/utils';
+
+const BaseCard = ({
+ url,
+ customHeaderImage,
+ schoolLogo,
+ title,
+ uuid,
+ subtitle,
+ variant,
+ productTypeCopy,
+ footer,
+ handleOnClick,
+ isLoading = false,
+}) => (
+
+
+
+
+
+
+
+
+ {productTypeCopy}
+
+
+
+ {footer}
+
+
+
+
+
+);
+
+BaseCard.propTypes = {
+ title: PropTypes.string.isRequired,
+ uuid: PropTypes.string.isRequired,
+ footer: PropTypes.element.isRequired,
+ productTypeCopy: PropTypes.string.isRequired,
+ subtitle: PropTypes.string.isRequired,
+ variant: PropTypes.string.isRequired,
+ url: PropTypes.string.isRequired,
+ customHeaderImage: PropTypes.string.isRequired,
+ schoolLogo: PropTypes.string.isRequired,
+ isLoading: PropTypes.bool,
+ handleOnClick: PropTypes.func.isRequired,
+};
+
+BaseCard.defaultProps = {
+ isLoading: false,
+};
+export default BaseCard;
diff --git a/src/recommendations/ProductCard/Footer/index.jsx b/src/recommendations/ProductCard/Footer/index.jsx
new file mode 100644
index 00000000..9c7caa01
--- /dev/null
+++ b/src/recommendations/ProductCard/Footer/index.jsx
@@ -0,0 +1,126 @@
+import React from 'react';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Icon } from '@edx/paragon';
+import { Launch } from '@edx/paragon/icons';
+import PropTypes from 'prop-types';
+
+import { isEdxProgram } from '../../data/utils';
+import {
+ cardFooterMessages,
+ externalLinkIconMessages,
+} from '../../messages';
+
+const ProductCardFooter = ({
+ factoid,
+ quickFacts,
+ courseLength,
+ footerMessage,
+ cardType,
+ is2UDegreeProgram,
+ isSubscriptionView,
+}) => {
+ const intl = useIntl();
+ const courseLengthLabel = courseLength > 1 ? 'Courses' : 'Course';
+
+ if (isSubscriptionView) {
+ return (
+
+ {intl.formatMessage(
+ cardFooterMessages[
+ 'recommendation.2u-product-card.footer-text.number-of-courses'
+ ],
+ { length: courseLength, label: courseLengthLabel },
+ )}
+ •
+
+ {intl.formatMessage(
+ cardFooterMessages[
+ 'recommendation.2u-product-card.footer-text.subscription'
+ ],
+ )}
+
+
+ );
+ }
+
+ if (footerMessage) {
+ return (
+
+ );
+ }
+
+ if (courseLength) {
+ return (
+
+ {intl.formatMessage(
+ cardFooterMessages[
+ 'recommendation.2u-product-card.footer-text.number-of-courses'
+ ],
+ { length: courseLength, label: courseLengthLabel },
+ )}
+
+ );
+ }
+
+ if (isEdxProgram({ cardType, is2UDegreeProgram })) {
+ if (quickFacts && quickFacts.length > 0) {
+ const quickFactsCount = quickFacts.length;
+
+ const threeFactsArrangement = [1, 3, 0];
+ const twoFactsArrangement = [0, 2];
+ return (
+ <>
+ {(quickFactsCount > 3 ? threeFactsArrangement : twoFactsArrangement)
+ .map((index) => quickFacts[index])
+ .filter(Boolean)
+ .map((fact, idx) => (
+
+ {idx > 0 && •}
+ {fact && fact.text}
+
+ ))}
+ >
+ );
+ }
+ }
+
+ if (factoid) {
+ return {factoid}
;
+ }
+
+ return null;
+};
+
+ProductCardFooter.propTypes = {
+ cardType: PropTypes.string,
+ factoid: PropTypes.string,
+ footerMessage: PropTypes.string,
+ quickFacts: PropTypes.arrayOf(PropTypes.shape({})),
+ courseLength: PropTypes.number,
+ is2UDegreeProgram: PropTypes.bool,
+ isSubscriptionView: PropTypes.bool,
+};
+
+ProductCardFooter.defaultProps = {
+ cardType: '',
+ factoid: '',
+ footerMessage: '',
+ quickFacts: [],
+ courseLength: undefined,
+ is2UDegreeProgram: false,
+ isSubscriptionView: false,
+};
+
+export default ProductCardFooter;
diff --git a/src/recommendations/ProductCard/index.jsx b/src/recommendations/ProductCard/index.jsx
new file mode 100644
index 00000000..726c459c
--- /dev/null
+++ b/src/recommendations/ProductCard/index.jsx
@@ -0,0 +1,129 @@
+import React from 'react';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+import PropTypes from 'prop-types';
+
+import BaseCard from './BaseCard';
+import Footer from './Footer';
+import { EXTERNAL_PRODUCT_SOURCES } from '../data/constants';
+import { createCodeFriendlyProduct, getVariant, useProductType } from '../data/utils';
+import {
+ cardBadgesMessages,
+ cardFooterMessages,
+} from '../messages';
+import { trackRecommendationCardClickOptimizely } from '../optimizelyExperiment';
+import { trackRecommendationsClicked } from '../track';
+
+const ProductCard = ({
+ product,
+ userId,
+ position,
+}) => {
+ const { formatMessage } = useIntl();
+
+ const productType = useProductType(product?.courseType, product?.type);
+
+ const variant = getVariant(productType);
+
+ const headerImage = product?.cardImageUrl || product?.image?.src;
+
+ const footerMessagesObj = {
+ [EXTERNAL_PRODUCT_SOURCES.EMERITUS]: formatMessage(
+ cardFooterMessages['recommendation.2u-product-card.footer-text.emeritus'],
+ ),
+ [EXTERNAL_PRODUCT_SOURCES.SHORELIGHT]: formatMessage(
+ cardFooterMessages['recommendation.2u-product-card.footer-text.shorelight'],
+ ),
+ };
+
+ const schoolName = product?.organizationShortCodeOverride
+ || product?.owners?.[0]?.name
+ || product?.authoringOrganizations?.[0]?.name
+ || product?.partner;
+ const schoolLogo = product?.organizationLogoOverrideUrl
+ || product?.logoFilename
+ || product?.authoringOrganizations?.[0]?.logoImageUrl
+ || product?.owners?.[0]?.logoImageUrl;
+
+ const { owners } = product;
+ const multipleSchoolNames = [];
+ const isMultipleOwner = owners?.length > 1;
+
+ if ((owners?.length > 1)) {
+ owners.forEach((owner, index, arr) => {
+ let school;
+ if (index === arr.length - 1) {
+ school = (
+ {owner.name}
+ );
+ } else {
+ school = (
+ <>
+ {owner.name}
+
+ >
+ );
+ }
+
+ multipleSchoolNames.push(school);
+ });
+ }
+
+ const productTypeCopy = formatMessage(
+ cardBadgesMessages[
+ `recommendation.2u-product-card.pill-text.${createCodeFriendlyProduct(productType)}`
+ ],
+ );
+ const handleCardClick = () => {
+ trackRecommendationCardClickOptimizely(userId?.toString());
+ trackRecommendationsClicked(
+ product.courseKey,
+ false,
+ position + 1,
+ userId,
+ product.marketingUrl,
+ product.recommendationType || 'algolia',
+ );
+ };
+
+ return (
+
+ )}
+ handleOnClick={handleCardClick}
+ isSubscriptionView={!!product.subscriptionEligible}
+ />
+ );
+};
+
+ProductCard.propTypes = {
+ product: PropTypes.shape([
+ PropTypes.shape({}),
+ ]).isRequired,
+ userId: PropTypes.number.isRequired,
+ position: PropTypes.number.isRequired,
+};
+
+ProductCard.defaultProps = {
+};
+export default ProductCard;
diff --git a/src/recommendations/RecommendationCard.jsx b/src/recommendations/RecommendationCard.jsx
deleted file mode 100644
index 2dab3b64..00000000
--- a/src/recommendations/RecommendationCard.jsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import React from 'react';
-
-import { Card, Hyperlink } from '@edx/paragon';
-import PropTypes from 'prop-types';
-
-import { trackRecommendationCardClickOptimizely } from './optimizelyExperiment';
-import { trackRecommendationsClicked } from './track';
-
-const RecommendationCard = (props) => {
- const { recommendation, position, userId } = props;
- const showPartnerLogo = recommendation.owners.length === 1;
-
- const getOwners = () => {
- if (recommendation.owners.length === 1) {
- return recommendation.owners[0].key;
- }
-
- let keys = '';
- recommendation.owners.forEach((owner) => {
- keys += `${owner.key }, `;
- });
- return keys.slice(0, -2);
- };
-
- const handleCardClick = () => {
- trackRecommendationCardClickOptimizely(userId?.toString());
- trackRecommendationsClicked(
- recommendation.courseKey,
- false,
- position + 1,
- userId,
- recommendation.marketingUrl,
- recommendation.recommendationType || 'algolia',
- );
- };
-
- return (
-
-
-
-
-
-
- Course} />
-
-
-
- );
-};
-
-RecommendationCard.propTypes = {
- recommendation: PropTypes.shape({
- courseKey: PropTypes.string.isRequired,
- activeRunKey: PropTypes.string.isRequired,
- title: PropTypes.string.isRequired,
- cardImageUrl: PropTypes.string.isRequired,
- owners: PropTypes.arrayOf(PropTypes.shape({
- key: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
- logoImageUrl: PropTypes.string.isRequired,
- })),
- marketingUrl: PropTypes.string.isRequired,
- recommendationType: PropTypes.string,
- }).isRequired,
- position: PropTypes.number.isRequired,
- userId: PropTypes.number,
-};
-
-RecommendationCard.defaultProps = {
- userId: null,
-};
-
-export default RecommendationCard;
diff --git a/src/recommendations/RecommendationsList.jsx b/src/recommendations/RecommendationsList.jsx
index 9b0b76f8..44c8b516 100644
--- a/src/recommendations/RecommendationsList.jsx
+++ b/src/recommendations/RecommendationsList.jsx
@@ -1,24 +1,50 @@
import React from 'react';
-import { Container } from '@edx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Container, Dropdown, DropdownButton } from '@edx/paragon';
import PropTypes from 'prop-types';
-import RecommendationCard from './RecommendationCard';
+import { RECOMMENDATIONS_OPTION_LIST } from './data/constants';
+import messages from './messages';
+import ProductCard from './ProductCard';
const RecommendationsList = (props) => {
- const { title, recommendations, userId } = props;
+ const { formatMessage } = useIntl();
+
+ const {
+ title, recommendations, userId, setSelectedRecommendationsType, selectedRecommendationsType,
+ } = props;
return (
{title}
+
+ {formatMessage(messages[`recommendation.option.${selectedRecommendationsType.value}`])}
+ >
+ )}
+ className="bg-white mt-5.5 mb-3"
+ >
+ {RECOMMENDATIONS_OPTION_LIST.map((option) => (
+ setSelectedRecommendationsType(option)}
+ id={`option-${option.value}`}
+ key={`option-${option.value}`}
+ >
+ {formatMessage(messages[`recommendation.option.${option.value}`])}
+
+ ))}
+
{
recommendations.map((recommendation, idx) => (
-
@@ -31,6 +57,11 @@ const RecommendationsList = (props) => {
RecommendationsList.propTypes = {
title: PropTypes.string.isRequired,
+ setSelectedRecommendationsType: PropTypes.func.isRequired,
+ selectedRecommendationsType: PropTypes.shape({
+ title: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired,
+ }).isRequired,
recommendations: PropTypes.arrayOf(PropTypes.shape({
courseKey: PropTypes.string.isRequired,
activeRunKey: PropTypes.string.isRequired,
diff --git a/src/recommendations/RecommendationsPage.jsx b/src/recommendations/RecommendationsPage.jsx
index 1ef00def..2f91101b 100644
--- a/src/recommendations/RecommendationsPage.jsx
+++ b/src/recommendations/RecommendationsPage.jsx
@@ -1,19 +1,16 @@
-import React, { useEffect, useState } from 'react';
+import React, { useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
- Hyperlink, Image, Spinner, StatefulButton,
+ Hyperlink, Image, StatefulButton,
} from '@edx/paragon';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
-import { EDUCATION_LEVEL_MAPPING, RECOMMENDATIONS_COUNT } from './data/constants';
-import getPersonalizedRecommendations from './data/service';
-import { convertCourseRunKeytoCourseKey } from './data/utils';
+import { RECOMMENDATIONS_COUNT, RECOMMENDATIONS_OPTION_LIST } from './data/constants';
import messages from './messages';
import RecommendationsList from './RecommendationsList';
-import { trackRecommendationsViewed } from './track';
import { DEFAULT_REDIRECT_URL } from '../data/constants';
const RecommendationsPage = (props) => {
@@ -23,51 +20,12 @@ const RecommendationsPage = (props) => {
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
const { formatMessage } = useIntl();
- const [isLoading, setIsLoading] = useState(true);
- const [recommendations, setRecommendations] = useState([]);
- const [algoliaRecommendations, setAlgoliaRecommendations] = useState([]);
- const educationLevel = EDUCATION_LEVEL_MAPPING[location.state?.educationLevel];
+ const [selectedRecommendationsType, setSelectedRecommendationsType] = useState(RECOMMENDATIONS_OPTION_LIST[1]);
+ const [recommendations] = useState([]);
- useEffect(() => {
- if (registrationResponse) {
- const generalRecommendations = JSON.parse(getConfig().GENERAL_RECOMMENDATIONS);
- let coursesWithKeys = [];
- getPersonalizedRecommendations(educationLevel).then((response) => {
- coursesWithKeys = response.map(course => ({
- ...course,
- courseKey: convertCourseRunKeytoCourseKey(course.activeRunKey),
- }));
- setAlgoliaRecommendations(coursesWithKeys.slice(0, RECOMMENDATIONS_COUNT));
-
- if (coursesWithKeys.length >= RECOMMENDATIONS_COUNT) {
- setRecommendations(coursesWithKeys.slice(0, RECOMMENDATIONS_COUNT));
- } else {
- const courseRecommendations = coursesWithKeys.concat(generalRecommendations);
- // Remove duplicate recommendations
- const uniqueRecommendations = courseRecommendations.filter(
- (recommendation, index, self) => index === self.findIndex((existingRecommendation) => (
- existingRecommendation.courseKey === recommendation.courseKey
- )),
- );
- setRecommendations(uniqueRecommendations.slice(0, RECOMMENDATIONS_COUNT));
- }
-
- setIsLoading(false);
- })
- .catch(() => {
- setRecommendations(generalRecommendations.slice(0, RECOMMENDATIONS_COUNT));
- setIsLoading(false);
- });
- }
- }, [registrationResponse, DASHBOARD_URL, educationLevel, userId]);
-
- useEffect(() => {
- if (!isLoading) {
- // We only want to track the recommendations returned by Algolia
- const courseKeys = algoliaRecommendations.map(course => course.courseKey);
- trackRecommendationsViewed(courseKeys, false, userId);
- }
- }, [isLoading, algoliaRecommendations, userId]);
+ const handleRecommendationType = (option) => {
+ setSelectedRecommendationsType(option);
+ };
if (!registrationResponse) {
global.location.assign(DASHBOARD_URL);
@@ -83,7 +41,7 @@ const RecommendationsPage = (props) => {
}
};
- if (!isLoading && recommendations.length < RECOMMENDATIONS_COUNT) {
+ if (recommendations.length < RECOMMENDATIONS_COUNT) {
handleRedirection();
}
@@ -106,12 +64,14 @@ const RecommendationsPage = (props) => {
- {(!isLoading && recommendations.length === RECOMMENDATIONS_COUNT) ? (
+ {(recommendations) && (
- )
- : (
-
- )}
+ )}
>
);
diff --git a/src/recommendations/data/constants.js b/src/recommendations/data/constants.js
index 914f049e..6d697368 100644
--- a/src/recommendations/data/constants.js
+++ b/src/recommendations/data/constants.js
@@ -9,3 +9,10 @@ export const EDUCATION_LEVEL_MAPPING = {
hs: 'Introductory',
jhs: 'Introductory',
};
+
+export const EXTERNAL_PRODUCT_SOURCES = {
+ EMERITUS: 'emeritus',
+ SHORELIGHT: 'shorelight',
+};
+
+export const RECOMMENDATIONS_OPTION_LIST = [{ title: 'Trending courses', value: 'trending' }, { title: 'Popular courses', value: 'popular' }];
diff --git a/src/recommendations/data/tests/utils.test.jsx b/src/recommendations/data/tests/utils.test.jsx
new file mode 100644
index 00000000..3a1d499e
--- /dev/null
+++ b/src/recommendations/data/tests/utils.test.jsx
@@ -0,0 +1,16 @@
+import { convertCourseRunKeytoCourseKey, useProductType } from '../utils';
+
+describe('UtilsTests', () => {
+ it('should return the courseKey after parsing the activeCourseRun key', async () => {
+ const courseKey = convertCourseRunKeytoCourseKey('course-v1:Demox+Test101+2023');
+ expect(courseKey).toEqual('Demox+Test101');
+ });
+ it('should return courseType and programType', async () => {
+ const programType = useProductType(undefined, 'Professional Certificate');
+ expect(programType).toEqual('Professional Certificate');
+ const courseType = useProductType('verified-audit', undefined);
+ expect(courseType).toEqual('Course');
+ const noCourseType = useProductType(undefined, undefined);
+ expect(noCourseType).toEqual(undefined);
+ });
+});
diff --git a/src/recommendations/data/utils.js b/src/recommendations/data/utils.js
index 09ec3e5f..e43ff0ef 100644
--- a/src/recommendations/data/utils.js
+++ b/src/recommendations/data/utils.js
@@ -12,6 +12,57 @@ export const convertCourseRunKeytoCourseKey = (courseRunKey) => {
return `${splitCourseKey[0]}+${splitCourseKey[1]}`;
};
-export default {
- convertCourseRunKeytoCourseKey,
+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",
};
+
+const programTypeToProductTypeMap = {
+ xseries: 'XSeries',
+ micromasters: 'MicroMasters',
+ microbachelors: 'MicroBachelors',
+ 'professional certificate': 'Professional Certificate',
+ "bachelor's": "Bachelor's",
+ bachelors: "Bachelor's",
+ "master's": "Master's",
+ masters: "Master's",
+ doctorate: 'Doctorate',
+ license: 'License',
+ certificate: 'Certificate',
+};
+
+export const useProductType = (courseType, programType) => {
+ const courseTypeLowerCase = courseType?.toLowerCase();
+ if (courseTypeToProductTypeMap[courseTypeLowerCase]) {
+ return courseTypeToProductTypeMap[courseTypeLowerCase];
+ }
+
+ const programTypeLowerCase = programType?.toLowerCase();
+ if (programTypeToProductTypeMap[programTypeLowerCase]) {
+ return programTypeToProductTypeMap[programTypeLowerCase];
+ }
+
+ return undefined;
+};
+
+export const getVariant = (productType) => (
+ ['Boot Camp', 'Executive Education', 'Course'].includes(productType) ? 'light' : 'dark'
+);
+
+export const createCodeFriendlyProduct = (type) => type?.replace(/\s+/g, '-').replace(/'/g, '').toLowerCase();
+
+export const isEdxProgram = ({ cardType, is2UDegreeProgram }) => cardType === 'program' && !is2UDegreeProgram;
+
+export const truncateText = (input) => (input?.length > 50 ? `${input.substring(0, 50)}...` : input);
+
+export default convertCourseRunKeytoCourseKey;
diff --git a/src/recommendations/messages.js b/src/recommendations/messages.js
index 55052cf0..63d9c46d 100644
--- a/src/recommendations/messages.js
+++ b/src/recommendations/messages.js
@@ -16,6 +16,111 @@ const messages = defineMessages({
defaultMessage: 'Skip for now',
description: 'Skip button text',
},
-
+ 'recommendation.option.trending': {
+ id: 'recommendation.option.trending',
+ defaultMessage: 'Trending Courses',
+ description: 'Trending courses option',
+ },
+ 'recommendation.option.popular': {
+ id: 'recommendation.option.popular',
+ defaultMessage: 'Popular Courses',
+ description: 'Popular courses option',
+ },
});
+
+export const cardBadgesMessages = defineMessages({
+ 'recommendation.2u-product-card.pill-text.course': {
+ id: 'recommendation.2u-product-card.pill-text.course',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'Course',
+ },
+ 'recommendation.2u-product-card.pill-text.microbachelors': {
+ id: 'recommendation.2u-product-card.pill-text.microbachelors',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'MicroBachelors®',
+ },
+ 'recommendation.2u-product-card.pill-text.micromasters': {
+ id: 'recommendation.2u-product-card.pill-text.micromasters',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'MicroMasters®',
+ },
+ 'recommendation.2u-product-card.pill-text.xseries': {
+ id: 'recommendation.2u-product-card.pill-text.xseries',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'XSeries',
+ },
+ 'recommendation.2u-product-card.pill-text.professional-certificate': {
+ id: 'recommendation.2u-product-card.pill-text.professional-certificate',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'Professional Certificate',
+ },
+ // 2U Products
+ 'recommendation.2u-product-card.pill-text.executive-education': {
+ id: 'recommendation.2u-product-card.pill-text.executive-education',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'Executive Education',
+ },
+ 'recommendation.2u-product-card.pill-text.boot-camp': {
+ id: 'recommendation.2u-product-card.pill-text.boot-camp',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'Boot Camp',
+ },
+ 'recommendation.2u-product-card.pill-text.bachelors': {
+ id: 'recommendation.2u-product-card.pill-text.bachelors',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'Bachelor\'s Degree',
+ },
+ 'recommendation.2u-product-card.pill-text.masters': {
+ id: 'recommendation.2u-product-card.pill-text.masters',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'Master\'s Degree',
+ },
+ 'recommendation.2u-product-card.pill-text.doctorate': {
+ id: 'recommendation.2u-product-card.pill-text.doctorate',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'Doctoral Program',
+ },
+ 'recommendation.2u-product-card.pill-text.certificate': {
+ id: 'recommendation.2u-product-card.pill-text.certificate',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'Certificate Program',
+ },
+ 'recommendation.2u-product-card.pill-text.license': {
+ id: 'recommendation.2u-product-card.pill-text.license',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'Licensure Program',
+ },
+});
+
+export const cardFooterMessages = defineMessages({
+ 'recommendation.2u-product-card.footer-text.emeritus': {
+ id: 'recommendation.2u-product-card.pill-text.emeritus',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'Offered on Emeritus',
+ },
+ 'recommendation.2u-product-card.footer-text.shorelight': {
+ id: 'recommendation.2u-product-card.pill-text.shorelight',
+ description: 'Text on a product card that describes which product line this item belongs to',
+ defaultMessage: 'Offered through Shorelight',
+ },
+ 'recommendation.2u-product-card.footer-text.number-of-courses': {
+ id: 'recommendation.2u-product-card.footer-text.number-of-courses',
+ description: 'Label in card footer that shows how many courses are in a program',
+ defaultMessage: '{length} {label}',
+ },
+ 'recommendation.2u-product-card.footer-text.subscription': {
+ id: 'recommendation.2u-product-card.footer-text.subscription',
+ description: 'Label in card footer that describes that it is a subscription program',
+ defaultMessage: 'Subscription',
+ },
+});
+
+export const externalLinkIconMessages = defineMessages({
+ 'recommendation.2u-product-card.launch-icon.sr-text': {
+ id: 'recommendation.2u-product-card.launch-icon.sr-text',
+ description: 'Screen reader text for the launch icon on the cards',
+ defaultMessage: 'Opens a link in a new tab',
+ },
+});
+
export default messages;
diff --git a/src/recommendations/tests/RecommendationsList.test.jsx b/src/recommendations/tests/RecommendationsList.test.jsx
new file mode 100644
index 00000000..19371bc0
--- /dev/null
+++ b/src/recommendations/tests/RecommendationsList.test.jsx
@@ -0,0 +1,140 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+
+import { getConfig, mergeConfig } from '@edx/frontend-platform';
+import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
+import { mount } from 'enzyme';
+import { act } from 'react-dom/test-utils';
+import configureStore from 'redux-mock-store';
+
+import mockedCoursesData from './mockedData';
+import { trackRecommendationCardClickOptimizely } from '../optimizelyExperiment';
+import RecommendationList from '../RecommendationsList';
+
+const IntlRecommendationList = injectIntl(RecommendationList);
+const mockStore = configureStore();
+
+jest.mock('@edx/frontend-platform/analytics', () => ({
+ sendTrackEvent: jest.fn(),
+}));
+jest.mock('../data/service', () => ({
+ __esModule: true,
+ default: jest.fn(),
+}));
+jest.mock('../optimizelyExperiment', () => ({
+ trackRecommendationCardClickOptimizely: jest.fn(),
+}));
+
+describe('RecommendationsListTests', () => {
+ mergeConfig({
+ GENERAL_RECOMMENDATIONS: '[]',
+ });
+
+ let defaultProps = {};
+ let store = {};
+
+ const registrationResult = {
+ redirectUrl: getConfig().LMS_BASE_URL.concat('/course-about-page-url'),
+ success: true,
+ };
+ const reduxWrapper = children => (
+
+ {children}
+
+ );
+
+ const getRecommendationsList = async (props = defaultProps) => {
+ const recommendationsPage = mount(reduxWrapper());
+ await act(async () => {
+ await Promise.resolve(recommendationsPage);
+ recommendationsPage.update();
+ });
+
+ return recommendationsPage;
+ };
+
+ beforeEach(() => {
+ store = mockStore({});
+ defaultProps = {
+ location: {
+ state: {
+ registrationResult,
+ userId: 111,
+ },
+ },
+ };
+ });
+
+ it('should render the product card with formatted message in card', async () => {
+ const props = {
+ recommendations: [mockedCoursesData[4]],
+ title: 'We have a few recommendations to get you started.',
+ userId: 1234567,
+ setSelectedRecommendationsType: jest.fn(),
+ selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
+ };
+ const recommendationsList = await getRecommendationsList(props);
+ expect(recommendationsList.find('.x-small').at(0).text()).toEqual('Offered on Emeritus');
+ });
+
+ it('should call trackRecommendationCardClickOptimizely when card is clicked', async () => {
+ const props = {
+ recommendations: [mockedCoursesData[1]],
+ title: 'We have a few recommendations to get you started.',
+ userId: 1234567,
+ setSelectedRecommendationsType: jest.fn(),
+ selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
+ };
+ const recommendationsList = await getRecommendationsList(props);
+ recommendationsList.find('.card-box').first().simulate('click');
+ expect(trackRecommendationCardClickOptimizely).toHaveBeenCalledTimes(1);
+ });
+
+ it('should render the recommendations card with with facets', async () => {
+ const props = {
+ recommendations: [mockedCoursesData[1]],
+ title: 'We have a few recommendations to get you started.',
+ userId: 1234567,
+ setSelectedRecommendationsType: jest.fn(),
+ selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
+ };
+ const recommendationsList = await getRecommendationsList(props);
+ expect(recommendationsList.find('.d-inline-block').at(0).text()).toEqual('6 Modules');
+ });
+
+ it('should render the recommendations card with footer content when subscription view is enabled', async () => {
+ const props = {
+ recommendations: [mockedCoursesData[3]],
+ title: 'We have a few recommendations to get you started.',
+ userId: 1234567,
+ setSelectedRecommendationsType: jest.fn(),
+ selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
+ };
+ const recommendationsList = await getRecommendationsList(props);
+ expect(recommendationsList.find('.d-inline-block').at(1).text()).toEqual('Subscription');
+ });
+
+ it('should render the recommendations card with footer content', async () => {
+ const props = {
+ recommendations: [mockedCoursesData[0]],
+ title: 'We have a few recommendations to get you started.',
+ userId: 1234567,
+ setSelectedRecommendationsType: jest.fn(),
+ selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
+ };
+ const recommendationsList = await getRecommendationsList(props);
+ expect(recommendationsList.find('.x-small').text()).toEqual('4 Courses');
+ });
+
+ it('should render the recommendations list', async () => {
+ const props = {
+ recommendations: mockedCoursesData,
+ title: 'We have a few recommendations to get you started.',
+ userId: 1234567,
+ setSelectedRecommendationsType: jest.fn(),
+ selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
+ };
+ const recommendationsList = await getRecommendationsList(props);
+ expect(recommendationsList.find('.recommendation-card').length).toBe(5);
+ });
+});
diff --git a/src/recommendations/tests/RecommendationsPage.test.jsx b/src/recommendations/tests/RecommendationsPage.test.jsx
index 856afef5..81a70c11 100644
--- a/src/recommendations/tests/RecommendationsPage.test.jsx
+++ b/src/recommendations/tests/RecommendationsPage.test.jsx
@@ -2,16 +2,13 @@ import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
-import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import configureStore from 'redux-mock-store';
-import { mockedGeneralRecommendations, mockedResponse } from './mockedData';
import { DEFAULT_REDIRECT_URL } from '../../data/constants';
import getPersonalizedRecommendations from '../data/service';
-import { trackRecommendationCardClickOptimizely } from '../optimizelyExperiment';
import RecommendationsPage from '../RecommendationsPage';
const IntlRecommendationsPage = injectIntl(RecommendationsPage);
@@ -24,9 +21,6 @@ jest.mock('../data/service', () => ({
__esModule: true,
default: jest.fn(),
}));
-jest.mock('../optimizelyExperiment', () => ({
- trackRecommendationCardClickOptimizely: jest.fn(),
-}));
describe('RecommendationsPageTests', () => {
mergeConfig({
@@ -68,19 +62,6 @@ describe('RecommendationsPageTests', () => {
};
});
- it('redirects to dashboard if user tries to access the page directly', async () => {
- const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
- delete window.location;
- window.location = {
- href: getConfig().BASE_URL,
- assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
- };
- getPersonalizedRecommendations.mockImplementation(() => Promise.resolve([]));
- await getRecommendationsPage({});
-
- expect(getPersonalizedRecommendations).toHaveBeenCalledTimes(0);
- expect(window.location.href).toEqual(DASHBOARD_URL);
- });
it('redirects to dashboard if user click on skip button', async () => {
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
registrationResult = {
@@ -95,87 +76,39 @@ describe('RecommendationsPageTests', () => {
},
},
};
- getPersonalizedRecommendations.mockImplementation(() => Promise.resolve(mockedResponse));
const recommendationsPage = await getRecommendationsPage(props);
- recommendationsPage.find('button').simulate('click');
+ recommendationsPage.find('.pgn__stateful-btn-state-default').first().simulate('click');
expect(window.location.href).toEqual(DASHBOARD_URL);
});
- it('should call trackRecommendationCardClickOptimizely when card is clicked', async () => {
- getPersonalizedRecommendations.mockImplementation(() => Promise.resolve(mockedResponse));
- const recommendationsPage = await getRecommendationsPage();
- recommendationsPage.find('.card-box').first().simulate('click');
- expect(trackRecommendationCardClickOptimizely).toHaveBeenCalledTimes(1);
- });
-
- it('should show loading state to user', async () => {
- getPersonalizedRecommendations.mockImplementation(() => Promise.resolve(mockedResponse));
- await act(async () => {
- const recommendationsPage = mount(reduxWrapper());
- expect(recommendationsPage.find('.spinner--position-centered').exists()).toBeTruthy();
- });
- });
-
- it('should call getPersonalizedRecommendations', async () => {
- delete window.location;
- window.location = { assign: jest.fn() };
- getPersonalizedRecommendations.mockClear();
- getPersonalizedRecommendations.mockImplementation(() => Promise.resolve([]));
- await getRecommendationsPage();
-
- expect(getPersonalizedRecommendations).toHaveBeenCalledTimes(1);
- expect(sendTrackEvent).toHaveBeenCalledWith(
- 'edx.bi.user.recommendations.viewed',
- {
- page: 'authn_recommendations',
- course_key_array: [],
- amplitude_recommendations: false,
- is_control: false,
- user_id: 111,
- },
- );
- });
-
- it('should display recommendations returned by Algolia', async () => {
- getPersonalizedRecommendations.mockImplementation(() => Promise.resolve(mockedResponse));
+ it('should change the option when click the dropdown option', async () => {
const recommendationsPage = await getRecommendationsPage();
- expect(recommendationsPage.find('#course-recommendations').exists()).toBeTruthy();
- });
-
- it('should not display recommendations if error comes in while fetching the recommendations', async () => {
- getPersonalizedRecommendations.mockImplementation(() => Promise.reject(mockedResponse));
- const recommendationsPage = await getRecommendationsPage();
-
- expect(recommendationsPage.find('#recommendation-card').exists()).toBeFalsy();
+ recommendationsPage.find('#dropdown-basic-button').at(1).simulate('click');
+ recommendationsPage.find('.dropdown-item').at(0).simulate('click',
+ { target: { title: 'Trending courses', value: 'trending' } });
+ expect(recommendationsPage.find('#dropdown-basic-button').at(1).text()).toEqual('Trending Courses');
});
it('should redirect if recommended courses count is less than RECOMMENDATIONS_COUNT', async () => {
delete window.location;
window.location = { assign: jest.fn() };
- getPersonalizedRecommendations.mockImplementation(() => Promise.resolve([mockedResponse[0]]));
- const recommendationsPage = await getRecommendationsPage();
-
- expect(recommendationsPage.find('#course-recommendations').exists()).toBeFalsy();
- expect(window.location.href).toEqual(registrationResult.redirectUrl);
- });
-
- it('should not redirect if fallback recommendations are enabled', async () => {
- mergeConfig({
- GENERAL_RECOMMENDATIONS: mockedGeneralRecommendations,
- });
- getPersonalizedRecommendations.mockImplementation(() => Promise.resolve([]));
const recommendationsPage = await getRecommendationsPage();
expect(recommendationsPage.find('#course-recommendations').exists()).toBeTruthy();
});
- it('should display all owners for a course', async () => {
- getPersonalizedRecommendations.mockImplementation(() => Promise.resolve(mockedResponse));
- const recommendationsPage = await getRecommendationsPage();
+ it('redirects to dashboard if user tries to access the page directly', async () => {
+ const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
+ delete window.location;
+ window.location = {
+ href: getConfig().BASE_URL,
+ assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
+ };
- expect(
- recommendationsPage.find('.pgn__card-header-subtitle-md').getElements()[0].props.children,
- ).toEqual('firstOwnerX, secondOwnerX');
+ await getRecommendationsPage({});
+
+ expect(getPersonalizedRecommendations).toHaveBeenCalledTimes(0);
+ expect(window.location.href).toEqual(DASHBOARD_URL);
});
});
diff --git a/src/recommendations/tests/mockedData.js b/src/recommendations/tests/mockedData.js
index dd240d79..2615b2f0 100644
--- a/src/recommendations/tests/mockedData.js
+++ b/src/recommendations/tests/mockedData.js
@@ -1,79 +1,241 @@
-export const mockedResponse = [
+const mockedCoursesData = [
{
- title: 'How to Learn Online 1',
- marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-1',
- cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-1.png',
- activeRunKey: 'course-v1:test+testX+2018',
- owners: [
+ uuid: '1fcffef0-468a-483f-95aa-87e2d4d2c408',
+ title: 'Google Cloud Computing Foundations',
+ subtitle: 'The Google Cloud Computing Foundations courses provide an overview of concepts central to cloud basics, big data, and machine learning, and where and how Google Cloud fits in.',
+ cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/card_images/1fcffef0-468a-483f-95aa-87e2d4d2c408-fc006d5bbc06.png',
+ authoringOrganizations: [
{
- key: 'firstOwnerX',
- logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-1.png',
- name: 'first owner',
- },
- {
- key: 'secondOwnerX',
- logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-1.png',
- name: 'second owner',
+ key: 'GoogleCloud',
+ logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/7a30f2c7-0d59-4890-ab19-fc8324e9c7d6-6ff36c37bf48.png',
+ name: 'Google Cloud',
},
],
- objectId: 'course-how-to-learn-online-key-1',
+ courses: [
+ {
+ course: {
+ title: 'Google Cloud Computing Foundations: Cloud Computing Fundamentals',
+ topics: [],
+ },
+ },
+ {
+ course: null,
+ },
+ {
+ course: null,
+ },
+ {
+ course: null,
+ },
+ ],
+ type: 'Professional Certificate',
+ marketingPath: '/professional-certificate/google-cloud-computing-foundations',
+ organizationLogoOverrideUrl: null,
+ organizationShortCodeOverride: '',
+ productSource: {
+ name: 'edX',
+ slug: 'edx',
+ description: '',
+ },
+ status: 'active',
+ hidden: false,
+ inProspectus: true,
+ is2UDegreeProgram: false,
+ degree: null,
+ locationRestriction: null,
+ subscriptionEligible: null,
+ objectID: 'program-1fcffef0-468a-483f-95aa-87e2d4d2c408',
+ cardType: 'program',
+ cardIndex: 0,
},
{
- title: 'How to Learn Online 2',
- marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-2',
- cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-2.png',
- activeRunKey: 'course-v1:test+testX+2019',
- owners: [
+ uuid: 'ff89bff4-42c4-4728-ae8b-7e9276ebfcac',
+ title: 'Online Master’s Degree in Public Health',
+ subtitle: 'from Boston University',
+ cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/card_images/ff89bff4-42c4-4728-ae8b-7e9276ebfcac-18ae533b0500.jpeg',
+ authoringOrganizations: [
{
- key: 'testX',
- logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-2.png',
- name: 'test',
+ key: 'BUx',
+ logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/36cfd0bb-1d18-4355-ae44-cb946573df3c-1e18515c3e4b.png',
+ name: 'Boston University',
},
],
- objectId: 'course-how-to-learn-online-key-2',
+ courses: [],
+ type: 'Masters',
+ marketingPath: '/masters/online-masters-in-public-health-boston-university',
+ organizationLogoOverrideUrl: null,
+ organizationShortCodeOverride: '',
+ productSource: {
+ name: 'edX',
+ slug: 'edx',
+ description: '',
+ },
+ status: 'active',
+ hidden: false,
+ inProspectus: true,
+ is2UDegreeProgram: false,
+ degree: {
+ quickFacts: [
+ {
+ text: '$24,000',
+ icon: 'fa-dollar',
+ },
+ {
+ text: '6 Modules',
+ icon: 'fa-book',
+ },
+ {
+ text: 'Fully Online',
+ icon: 'fa-desktop',
+ },
+ {
+ text: '24 Months',
+ icon: 'fa-clock-o',
+ },
+ ],
+ additionalMetadata: null,
+ organizationLogoOverrideUrl: null,
+ },
+ locationRestriction: null,
+ subscriptionEligible: null,
+ objectID: 'program-ff89bff4-42c4-4728-ae8b-7e9276ebfcac',
+ cardType: 'program',
+ cardIndex: 1,
},
{
- title: 'How to Learn Online 3',
- marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-3',
- cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-3.png',
- activeRunKey: 'course-v1:test+testX+2020',
- owners: [
+ uuid: '123f9327-bfef-459f-8bf2-c53277aa58f8',
+ title: 'Bachelor of Science in Data Science and Business Analytics',
+ subtitle: '',
+ cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/card_images/123f9327-bfef-459f-8bf2-c53277aa58f8-60ed48b6667d.png',
+ authoringOrganizations: [
{
- key: 'testX',
- logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-3.png',
- name: 'test',
+ key: 'UniversityofLondon',
+ logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/5427ee88-6dba-46ff-8ed8-87b71e3234da-0af1465ab67e.png',
+ name: 'University of London',
},
],
- objectId: 'course-how-to-learn-online-key-3',
+ courses: [],
+ type: 'Bachelors',
+ marketingPath: '/bachelors/london-bachelor-of-science-in-data-science-and-business-analytics',
+ organizationLogoOverrideUrl: null,
+ organizationShortCodeOverride: 'University of London',
+ productSource: {
+ name: '2u',
+ slug: '2u',
+ description: '2U, Trilogy, Getsmarter -- external source for 2u courses and programs',
+ },
+ status: 'active',
+ hidden: false,
+ inProspectus: true,
+ is2UDegreeProgram: true,
+ degree: {
+ quickFacts: [],
+ additionalMetadata: {
+ externalIdentifier: '2ca2718e-643c-4430-b521-b9ebb91ad226',
+ externalUrl: 'https://programs.edx.org/requestinfo/lse?utm_source=edx&utm_medium=referral',
+ organicUrl: 'https://programs.edx.org/requestinfo/lse?utm_source=edx&utm_medium=referral',
+ },
+ organizationLogoOverrideUrl: null,
+ },
+ locationRestriction: {
+ restrictionType: 'blocklist',
+ countries: [
+ 'IN',
+ ],
+ states: [],
+ },
+ subscriptionEligible: null,
+ objectID: 'program-123f9327-bfef-459f-8bf2-c53277aa58f8',
+ cardType: 'program',
+ cardIndex: 0,
},
{
- title: 'How to Learn Online 4',
- marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-4',
- cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-4.png',
- activeRunKey: 'course-v1:test+testX+2021',
- owners: [
+ uuid: '51037067-2c57-4a58-a963-91918ccee5c8',
+ title: 'Leading in a Remote Environment',
+ subtitle: 'Remote work presents unique challenges to maintaining relationships and achieving goals. Leaders need to be adaptive in order to mobilize their teams to meet these new challenges.',
+ cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/card_images/51037067-2c57-4a58-a963-91918ccee5c8-b24ebade984c.jpeg',
+ authoringOrganizations: [
{
- key: 'testX',
- logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-4.png',
- name: 'test',
+ key: 'HarvardX',
+ logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/44022f13-20df-4666-9111-cede3e5dc5b6-2cc39992c67a.png',
+ name: 'Harvard University',
},
],
- objectId: 'course-how-to-learn-online-key-4',
+ courses: [
+ {
+ course: {
+ title: 'Exercising Leadership: Foundational Principles',
+ topics: [],
+ },
+ },
+ {
+ course: {
+ title: 'Remote Work Revolution for Everyone',
+ topics: [],
+ },
+ },
+ ],
+ type: 'Professional Certificate',
+ marketingPath: '/professional-certificate/harvardx-leading-in-a-remote-environment',
+ organizationLogoOverrideUrl: null,
+ organizationShortCodeOverride: '',
+ productSource: {
+ name: 'edX',
+ slug: 'edx',
+ description: '',
+ },
+ status: 'active',
+ hidden: false,
+ inProspectus: true,
+ is2UDegreeProgram: false,
+ degree: null,
+ locationRestriction: null,
+ subscriptionEligible: true,
+ objectID: 'program-51037067-2c57-4a58-a963-91918ccee5c8',
+ cardType: 'program',
+ cardIndex: 1,
},
{
- title: 'How to Learn Online 5',
- marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-5',
- cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-5.png',
- activeRunKey: 'course-v1:test+testX+2022',
- owners: [
+ uuid: '89f39d1a-bb23-4944-b6e0-b51fe25ce932',
+ title: 'Master of Science in Artificial Intelligence (MSAI)',
+ subtitle: '',
+ cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/card_images/89f39d1a-bb23-4944-b6e0-b51fe25ce932-c0b01712082d.jpg',
+ authoringOrganizations: [
{
- key: 'testX',
- logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-5.png',
- name: 'test',
+ key: 'UTAustinX',
+ logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/9d38ad58-87fb-4a89-9f23-c8df318112e3-aec8e9e98a5f.png',
+ name: 'The University of Texas at Austin',
},
],
- objectId: 'course-how-to-learn-online-key-5',
+ courses: [],
+ type: 'Masters',
+ marketingPath: '/masters/online-master-artificial-intelligence-utaustinx',
+ organizationLogoOverrideUrl: null,
+ organizationShortCodeOverride: '',
+ productSource: {
+ name: '2u',
+ slug: 'emeritus',
+ description: '2U, Trilogy, Getsmarter -- external source for 2u courses and programs',
+ },
+ status: 'active',
+ hidden: false,
+ inProspectus: true,
+ is2UDegreeProgram: true,
+ degree: {
+ quickFacts: [],
+ additionalMetadata: {
+ externalIdentifier: '89f39d1a-bb23-4944-b6e0-b51fe25ce932',
+ externalUrl: 'https://www.edx.org/masters/online-master-artificial-intelligence-utaustinx',
+ organicUrl: 'https://www.edx.org/masters/online-master-artificial-intelligence-utaustinx',
+ },
+ organizationLogoOverrideUrl: null,
+ },
+ locationRestriction: null,
+ subscriptionEligible: null,
+ objectID: 'program-89f39d1a-bb23-4944-b6e0-b51fe25ce932',
+ cardType: 'program',
+ cardIndex: 0,
},
];
-export const mockedGeneralRecommendations = '[{"courseKey":"test+text1","activeRunKey":"course-v1:test+test1+2018","cardImageUrl":"https://test-recommendations.com/text-1.jpg","marketingUrl":"https://test-recommendations.com/test-1","objectId":"test-1","owners":[{"key":"Testx","logoImageUrl":"https://test-recommendations.com/organization/test-1.png","name":"General recommendation org 1"}],"title":"General recommendation 1","recommendationType":"general"},{"courseKey":"test+text2","activeRunKey":"course-v1:test+test2+2018","cardImageUrl":"https://test-recommendations.com/text-2.jpg","marketingUrl":"https://test-recommendations.com/test-2","objectId":"test-2","owners":[{"key":"Testx","logoImageUrl":"https://test-recommendations.com/organization/test-2.png","name":"General recommendation org 2"}],"title":"General recommendation 2","recommendationType":"general"},{"courseKey":"test+text3","activeRunKey":"course-v1:test+test3+2018","cardImageUrl":"https://test-recommendations.com/text-3.jpg","marketingUrl":"https://test-recommendations.com/test-3","objectId":"test-3","owners":[{"key":"Testx","logoImageUrl":"https://test-recommendations.com/organization/test-3.png","name":"General recommendation org 3"}],"title":"General recommendation 3","recommendationType":"general"},{"courseKey":"test+text4","activeRunKey":"course-v1:test+test4+2018","cardImageUrl":"https://test-recommendations.com/text-4.jpg","marketingUrl":"https://test-recommendations.com/test-4","objectId":"test-4","owners":[{"key":"Testx","logoImageUrl":"https://test-recommendations.com/organization/test-4.png","name":"General recommendation org 4"}],"title":"General recommendation 4","recommendationType":"general"},{"courseKey":"test+text5","activeRunKey":"course-v1:test+test5+2018","cardImageUrl":"https://test-recommendations.com/text-5.jpg","marketingUrl":"https://test-recommendations.com/test-5","objectId":"test-5","owners":[{"key":"Testx","logoImageUrl":"https://test-recommendations.com/organization/test-5.png","name":"General recommendation org 5"}],"title":"General recommendation 5","recommendationType":"general"}]';
+export default mockedCoursesData;
diff --git a/src/sass/_recommendations_card_base.scss b/src/sass/_recommendations_card_base.scss
new file mode 100644
index 00000000..f9ee9018
--- /dev/null
+++ b/src/sass/_recommendations_card_base.scss
@@ -0,0 +1,133 @@
+$card-height: 332px;
+$header-height: 104px;
+
+.base-card-link {
+ text-decoration: none;
+}
+
+.base-card-link:hover {
+ text-decoration: none;
+}
+
+.base-card {
+ height: $card-height;
+
+ /* stylelint-disable selector-max-type */
+ p {
+ margin-bottom: 0;
+ }
+
+ .pgn__card-image-cap {
+ height: $header-height;
+
+ object: {
+ fit: cover;
+ position: top center;
+ }
+ }
+
+ .pgn__card-logo-cap {
+ bottom: -1.5rem;
+
+ object: {
+ fit: scale-down;
+ position: center center;
+ }
+ }
+
+ .pgn__card-header-title-md {
+ font: {
+ size: 1.125rem !important; // 18px
+ weight: 700;
+ }
+
+ line-height: 24px;
+ }
+
+ .pgn__card-header-subtitle-md {
+ font: {
+ size: 0.875rem !important; // 14px
+ weight: 400;
+ }
+
+ line-height: 24px;
+ }
+
+ .product-badge {
+ position: absolute;
+ bottom: 2.75rem;
+ }
+
+ .footer-content {
+ position: absolute;
+ bottom: 1rem;
+ }
+
+ &.light {
+ background-color: $white;
+
+ .title {
+ color: $black;
+ }
+
+ .subtitle {
+ color: $gray-700;
+ }
+
+ .badge {
+ background-color: $light-500;
+ color: $black;
+ }
+
+ .footer-content {
+ color: $gray-700;
+ }
+ }
+
+ &.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;
+ }
+ }
+}
+
+.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;
+}
+
+.base-card-image-show {
+ .pgn__card-image-cap {
+ display: block;
+ }
+
+ .pgn__card-logo-cap {
+ display: block !important;
+ }
+}
diff --git a/src/sass/_recommendations_page.scss b/src/sass/_recommendations_page.scss
index 9ae2e085..13aa6e0b 100644
--- a/src/sass/_recommendations_page.scss
+++ b/src/sass/_recommendations_page.scss
@@ -24,9 +24,29 @@
@include media-breakpoint-up(xxl) {
max-width: $max-width-lg + $grid-gutter-width !important;
}
+ .dropdown {
+ background-color: #FBFAF9 !important;
+
+ .dropdown-toggle {
+ background-color: #FBFAF9 !important;
+ border: none;
+ color: #000000;
+ font-weight: 700;
+ font-size: 22px;
+ }
+ .dropdown-toggle:hover {
+ background-color: #FBFAF9 !important;
+ border: none;
+ color: #000000;
+ }
+ .dropdown-toggle::after {
+ margin-left: 1.3em;
+ }
+ }
}
.recommendation-card {
+ cursor: pointer;
.pgn__hyperlink {
display: block;
}
@@ -65,3 +85,7 @@
line-height: 1.25rem;
}
}
+.footer-icon{
+ height: 16px;
+ width: 16px;
+}
diff --git a/src/sass/_style.scss b/src/sass/_style.scss
index da3c95de..d15d71cd 100644
--- a/src/sass/_style.scss
+++ b/src/sass/_style.scss
@@ -6,6 +6,7 @@
@import "_progressive_profiling_page.scss";
@import "_login_page.scss";
@import "_forgot_password.scss";
+@import "_recommendations_card_base.scss";
//
// ----------------------------
// #COLORS