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 ( +
+

{footerMessage}

+ +
+ ); + } + + 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) => { {getConfig().SITE_NAME}
- {(!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