feat: update static recommendation placement (#1002)
* feat: update static recommendation placement * refactor: clean recommendation code (#1003) --------- Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
This commit is contained in:
@@ -1,72 +1,27 @@
|
||||
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 (
|
||||
<p className="d-inline-block x-small">
|
||||
{intl.formatMessage(
|
||||
cardFooterMessages[
|
||||
'recommendation.2u-product-card.footer-text.number-of-courses'
|
||||
],
|
||||
{ length: courseLength, label: courseLengthLabel },
|
||||
)}
|
||||
<span className="p-2">•</span>
|
||||
<p className="d-inline-block">
|
||||
{intl.formatMessage(
|
||||
cardFooterMessages[
|
||||
'recommendation.2u-product-card.footer-text.subscription'
|
||||
],
|
||||
)}
|
||||
</p>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (footerMessage) {
|
||||
return (
|
||||
<div className="footer-message d-flex align-items-center">
|
||||
<p className="x-small">{footerMessage}</p>
|
||||
<Icon
|
||||
src={Launch}
|
||||
className="ml-1 footer-icon"
|
||||
screenReaderText={intl.formatMessage(
|
||||
externalLinkIconMessages[
|
||||
'recommendation.2u-product-card.launch-icon.sr-text'
|
||||
],
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (courseLength) {
|
||||
return (
|
||||
<p className="x-small">
|
||||
{intl.formatMessage(
|
||||
cardFooterMessages[
|
||||
'recommendation.2u-product-card.footer-text.number-of-courses'
|
||||
'recommendation.product-card.footer-text.number-of-courses'
|
||||
],
|
||||
{ length: courseLength, label: courseLengthLabel },
|
||||
)}
|
||||
@@ -74,7 +29,7 @@ const ProductCardFooter = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (isEdxProgram({ cardType, is2UDegreeProgram })) {
|
||||
if (cardType === 'program') {
|
||||
if (quickFacts && quickFacts.length > 0) {
|
||||
const quickFactsCount = quickFacts.length;
|
||||
|
||||
@@ -106,21 +61,15 @@ const ProductCardFooter = ({
|
||||
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;
|
||||
|
||||
@@ -5,11 +5,9 @@ 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';
|
||||
@@ -27,15 +25,6 @@ const ProductCard = ({
|
||||
|
||||
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
|
||||
@@ -71,7 +60,7 @@ const ProductCard = ({
|
||||
|
||||
const productTypeCopy = formatMessage(
|
||||
cardBadgesMessages[
|
||||
`recommendation.2u-product-card.pill-text.${createCodeFriendlyProduct(productType)}`
|
||||
`recommendation.product-card.pill-text.${createCodeFriendlyProduct(productType)}`
|
||||
],
|
||||
);
|
||||
const handleCardClick = () => {
|
||||
@@ -100,7 +89,6 @@ const ProductCard = ({
|
||||
variant={variant}
|
||||
footer={(
|
||||
<Footer
|
||||
footerMessage={footerMessagesObj?.[product.productSource?.slug]}
|
||||
quickFacts={product.degree?.quickFacts}
|
||||
externalUrl={product.additionalMetadata?.externalUrl
|
||||
|| product.degree?.additionalMetadata?.externalUrl}
|
||||
|
||||
@@ -1,79 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Container, Dropdown, DropdownButton } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { RECOMMENDATIONS_OPTION_LIST } from './data/constants';
|
||||
import messages from './messages';
|
||||
import ProductCard from './ProductCard';
|
||||
|
||||
const RecommendationsList = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const {
|
||||
title, recommendations, userId, setSelectedRecommendationsType, selectedRecommendationsType,
|
||||
} = props;
|
||||
const { recommendations, userId } = props;
|
||||
|
||||
return (
|
||||
<Container id="course-recommendations" size="lg" className="recommendations-container">
|
||||
<h2 className="text-sm-center mb-4 text-left recommendations-container__heading">
|
||||
{title}
|
||||
</h2>
|
||||
<DropdownButton
|
||||
id="dropdown-basic-button"
|
||||
title={(
|
||||
<>
|
||||
{formatMessage(messages[`recommendation.option.${selectedRecommendationsType.value}`])}
|
||||
</>
|
||||
)}
|
||||
className="bg-white mt-5.5 mb-3"
|
||||
>
|
||||
{RECOMMENDATIONS_OPTION_LIST.map((option) => (
|
||||
<Dropdown.Item
|
||||
onClick={() => setSelectedRecommendationsType(option)}
|
||||
id={`option-${option.value}`}
|
||||
key={`option-${option.value}`}
|
||||
>
|
||||
{formatMessage(messages[`recommendation.option.${option.value}`])}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</DropdownButton>
|
||||
<div className="d-flex recommendations-container__card-list">
|
||||
{
|
||||
recommendations.map((recommendation, idx) => (
|
||||
<div className="d-flex recommendations-container__card-list">
|
||||
{
|
||||
recommendations.map((recommendation, idx) => (
|
||||
<span key={recommendation.uuid}>
|
||||
<ProductCard
|
||||
key={recommendation.activeRunKey}
|
||||
product={recommendation}
|
||||
position={idx}
|
||||
userId={userId}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Container>
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
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,
|
||||
uuid: PropTypes.string,
|
||||
})),
|
||||
userId: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,38 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Hyperlink, Image, StatefulButton,
|
||||
Container, Hyperlink, Image, StatefulButton, Tab, Tabs,
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { RECOMMENDATIONS_COUNT, RECOMMENDATIONS_OPTION_LIST } from './data/constants';
|
||||
import messages from './messages';
|
||||
import RecommendationsList from './RecommendationsList';
|
||||
import { DEFAULT_REDIRECT_URL } from '../data/constants';
|
||||
|
||||
const RecommendationsPage = (props) => {
|
||||
const { location } = props;
|
||||
const RecommendationsPage = ({ location }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const registrationResponse = location.state?.registrationResult;
|
||||
const userId = location.state?.userId;
|
||||
|
||||
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
const POPULAR_PRODUCTS = JSON.parse(getConfig().POPULAR_PRODUCTS);
|
||||
const TRENDING_PRODUCTS = JSON.parse(getConfig().TRENDING_PRODUCTS);
|
||||
const { formatMessage } = useIntl();
|
||||
const [selectedRecommendationsType, setSelectedRecommendationsType] = useState(RECOMMENDATIONS_OPTION_LIST[1]);
|
||||
const [recommendations, setRecommendations] = useState(POPULAR_PRODUCTS);
|
||||
|
||||
const handleRecommendationType = (option) => {
|
||||
setSelectedRecommendationsType(option);
|
||||
setRecommendations(option.value === 'popular' ? POPULAR_PRODUCTS : TRENDING_PRODUCTS);
|
||||
};
|
||||
|
||||
if (!registrationResponse) {
|
||||
global.location.assign(DASHBOARD_URL);
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleRedirection = () => {
|
||||
window.history.replaceState(location.state, null, '');
|
||||
@@ -43,15 +31,20 @@ const RecommendationsPage = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (recommendations.length < RECOMMENDATIONS_COUNT) {
|
||||
handleRedirection();
|
||||
}
|
||||
|
||||
const handleSkip = (e) => {
|
||||
e.preventDefault();
|
||||
handleRedirection();
|
||||
};
|
||||
|
||||
if (!registrationResponse) {
|
||||
window.location.href = DASHBOARD_URL;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!POPULAR_PRODUCTS.length || !TRENDING_PRODUCTS.length) {
|
||||
handleRedirection();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -66,28 +59,42 @@ const RecommendationsPage = (props) => {
|
||||
<Image className="logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
||||
</Hyperlink>
|
||||
</div>
|
||||
{(recommendations) && (
|
||||
<div className="d-flex flex-column align-items-center justify-content-center flex-grow-1 p-1">
|
||||
<RecommendationsList
|
||||
title={formatMessage(messages['recommendation.page.heading'])}
|
||||
recommendations={recommendations}
|
||||
userId={userId}
|
||||
setSelectedRecommendationsType={handleRecommendationType}
|
||||
selectedRecommendationsType={selectedRecommendationsType}
|
||||
<div className="d-flex flex-column align-items-center justify-content-center flex-grow-1 p-1">
|
||||
<Container id="course-recommendations" size="lg" className="recommendations-container">
|
||||
<h2 className="text-sm-center mb-4 text-left recommendations-container__heading">
|
||||
{formatMessage(messages['recommendation.page.heading'])}
|
||||
</h2>
|
||||
<Tabs
|
||||
variant="tabs"
|
||||
defaultActiveKey="popular"
|
||||
id="recommendations-selection"
|
||||
>
|
||||
<Tab tabClassName="mb-3" eventKey="popular" title={formatMessage(messages['recommendation.option.popular'])}>
|
||||
<RecommendationsList
|
||||
recommendations={POPULAR_PRODUCTS}
|
||||
userId={userId}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab tabClassName="mb-3" eventKey="trending" title={formatMessage(messages['recommendation.option.trending'])}>
|
||||
<RecommendationsList
|
||||
recommendations={TRENDING_PRODUCTS}
|
||||
userId={userId}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Container>
|
||||
<div className="text-center">
|
||||
<StatefulButton
|
||||
className="font-weight-500"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
labels={{
|
||||
default: formatMessage(messages['recommendation.skip.button']),
|
||||
}}
|
||||
onClick={handleSkip}
|
||||
/>
|
||||
<div className="text-center">
|
||||
<StatefulButton
|
||||
className="font-weight-500"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
labels={{
|
||||
default: formatMessage(messages['recommendation.skip.button']),
|
||||
}}
|
||||
onClick={handleSkip}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -100,7 +107,6 @@ RecommendationsPage.propTypes = {
|
||||
redirectUrl: PropTypes.string,
|
||||
}),
|
||||
userId: PropTypes.number,
|
||||
educationLevel: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
|
||||
|
||||
@@ -9,10 +9,3 @@ 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' }];
|
||||
|
||||
@@ -20,24 +20,10 @@ const courseTypeToProductTypeMap = {
|
||||
'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',
|
||||
};
|
||||
|
||||
@@ -61,8 +47,6 @@ export const getVariant = (productType) => (
|
||||
|
||||
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;
|
||||
|
||||
@@ -18,106 +18,55 @@ const messages = defineMessages({
|
||||
},
|
||||
'recommendation.option.trending': {
|
||||
id: 'recommendation.option.trending',
|
||||
defaultMessage: 'Trending Courses',
|
||||
description: 'Trending courses option',
|
||||
defaultMessage: 'Trending',
|
||||
description: 'Title for trending products',
|
||||
},
|
||||
'recommendation.option.popular': {
|
||||
id: 'recommendation.option.popular',
|
||||
defaultMessage: 'Popular Courses',
|
||||
description: 'Popular courses option',
|
||||
defaultMessage: 'Most Popular',
|
||||
description: 'Title for popular products',
|
||||
},
|
||||
});
|
||||
|
||||
export const cardBadgesMessages = defineMessages({
|
||||
'recommendation.2u-product-card.pill-text.course': {
|
||||
id: 'recommendation.2u-product-card.pill-text.course',
|
||||
'recommendation.product-card.pill-text.course': {
|
||||
id: 'recommendation.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',
|
||||
'recommendation.product-card.pill-text.professional-certificate': {
|
||||
id: 'recommendation.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',
|
||||
'recommendation.product-card.footer-text.emeritus': {
|
||||
id: 'recommendation.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',
|
||||
'recommendation.product-card.footer-text.shorelight': {
|
||||
id: 'recommendation.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',
|
||||
'recommendation.product-card.footer-text.number-of-courses': {
|
||||
id: 'recommendation.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',
|
||||
'recommendation.product-card.footer-text.subscription': {
|
||||
id: 'recommendation.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',
|
||||
'recommendation.product-card.launch-icon.sr-text': {
|
||||
id: 'recommendation.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',
|
||||
},
|
||||
|
||||
@@ -1,140 +1,42 @@
|
||||
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 mockedProductData from './mockedData';
|
||||
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 store = mockStore({});
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const getRecommendationsList = async (props = defaultProps) => {
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationList {...props} />));
|
||||
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', () => {
|
||||
const props = {
|
||||
recommendations: mockedProductData,
|
||||
userId: 1234567,
|
||||
};
|
||||
|
||||
const recommendationsList = mount(reduxWrapper(<IntlRecommendationList {...props} />));
|
||||
expect(recommendationsList.find('.recommendation-card').length).toEqual(mockedProductData.length);
|
||||
});
|
||||
|
||||
it('should render the product card with formatted message in card', async () => {
|
||||
it('should render the recommendations card with footer content', () => {
|
||||
const props = {
|
||||
recommendations: [mockedCoursesData[4]],
|
||||
title: 'We have a few recommendations to get you started.',
|
||||
recommendations: mockedProductData,
|
||||
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);
|
||||
const recommendationsList = mount(reduxWrapper(<IntlRecommendationList {...props} />));
|
||||
expect(recommendationsList.find('.x-small').at(0).text()).toEqual('1 Course');
|
||||
expect(recommendationsList.find('.x-small').at(1).text()).toEqual('2 Courses');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,9 @@ 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 { DEFAULT_REDIRECT_URL } from '../../data/constants';
|
||||
import getPersonalizedRecommendations from '../data/service';
|
||||
import RecommendationsPage from '../RecommendationsPage';
|
||||
|
||||
const IntlRecommendationsPage = injectIntl(RecommendationsPage);
|
||||
@@ -32,8 +30,11 @@ describe('RecommendationsPageTests', () => {
|
||||
let defaultProps = {};
|
||||
let store = {};
|
||||
|
||||
let registrationResult = {
|
||||
redirectUrl: getConfig().LMS_BASE_URL.concat('/course-about-page-url'),
|
||||
const dashboardUrl = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
const redirectUrl = getConfig().LMS_BASE_URL.concat('/course-about-page-url');
|
||||
|
||||
const registrationResult = {
|
||||
redirectUrl,
|
||||
success: true,
|
||||
};
|
||||
const reduxWrapper = children => (
|
||||
@@ -42,16 +43,6 @@ describe('RecommendationsPageTests', () => {
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const getRecommendationsPage = async (props = defaultProps) => {
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage {...props} />));
|
||||
await act(async () => {
|
||||
await Promise.resolve(recommendationsPage);
|
||||
recommendationsPage.update();
|
||||
});
|
||||
|
||||
return recommendationsPage;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore({});
|
||||
defaultProps = {
|
||||
@@ -64,53 +55,25 @@ describe('RecommendationsPageTests', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('redirects to dashboard if user click on skip button', async () => {
|
||||
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
registrationResult = {
|
||||
...registrationResult,
|
||||
redirectUrl: getConfig().LMS_BASE_URL.concat('/dashboard'),
|
||||
};
|
||||
const props = {
|
||||
location: {
|
||||
state: {
|
||||
registrationResult,
|
||||
userId: 111,
|
||||
},
|
||||
},
|
||||
};
|
||||
const recommendationsPage = await getRecommendationsPage(props);
|
||||
it('should redirect to dashboard if user is not coming from registration workflow', () => {
|
||||
mount(reduxWrapper(<IntlRecommendationsPage />));
|
||||
expect(window.location.href).toEqual(dashboardUrl);
|
||||
});
|
||||
|
||||
it('should redirect if either popular or trending recommendations are not configured', () => {
|
||||
mount(reduxWrapper(<IntlRecommendationsPage {...defaultProps} />));
|
||||
expect(window.location.href).toEqual(redirectUrl);
|
||||
});
|
||||
|
||||
it('should redirect user if they click "Skip for now" button', () => {
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage {...defaultProps} />));
|
||||
recommendationsPage.find('.pgn__stateful-btn-state-default').first().simulate('click');
|
||||
expect(window.location.href).toEqual(DASHBOARD_URL);
|
||||
|
||||
expect(window.location.href).toEqual(redirectUrl);
|
||||
});
|
||||
|
||||
it('should change the option when click the dropdown option', async () => {
|
||||
const recommendationsPage = await getRecommendationsPage();
|
||||
|
||||
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() };
|
||||
const recommendationsPage = await getRecommendationsPage();
|
||||
|
||||
expect(recommendationsPage.find('#course-recommendations').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
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; }),
|
||||
};
|
||||
|
||||
await getRecommendationsPage({});
|
||||
|
||||
expect(getPersonalizedRecommendations).toHaveBeenCalledTimes(0);
|
||||
expect(window.location.href).toEqual(DASHBOARD_URL);
|
||||
it('displays popular products as default recommendations', () => {
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage {...defaultProps} />));
|
||||
expect(recommendationsPage.find('.nav-link .active a').text()).toEqual('Most Popular');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,241 +1,80 @@
|
||||
const mockedCoursesData = [
|
||||
const mockedProductData = [
|
||||
{
|
||||
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',
|
||||
uuid: 'test-uuid-1',
|
||||
title: 'How to Learn Online 1',
|
||||
subtitle: 'Org 1',
|
||||
cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-1.png',
|
||||
authoringOrganizations: [
|
||||
{
|
||||
key: 'GoogleCloud',
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/7a30f2c7-0d59-4890-ab19-fc8324e9c7d6-6ff36c37bf48.png',
|
||||
name: 'Google Cloud',
|
||||
key: 'org-1',
|
||||
logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-1.png',
|
||||
name: 'Org 1',
|
||||
},
|
||||
],
|
||||
courses: [
|
||||
{
|
||||
course: {
|
||||
title: 'Google Cloud Computing Foundations: Cloud Computing Fundamentals',
|
||||
title: 'How to learn online course 1',
|
||||
topics: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
course: null,
|
||||
},
|
||||
{
|
||||
course: null,
|
||||
},
|
||||
{
|
||||
course: null,
|
||||
},
|
||||
],
|
||||
type: 'Professional Certificate',
|
||||
marketingPath: '/professional-certificate/google-cloud-computing-foundations',
|
||||
url: '/test-professional-certificate/how-to-learn-online-1',
|
||||
organizationLogoOverrideUrl: null,
|
||||
organizationShortCodeOverride: '',
|
||||
productSource: {
|
||||
name: 'edX',
|
||||
slug: 'edx',
|
||||
description: '',
|
||||
name: 'company X',
|
||||
slug: 'companyX',
|
||||
},
|
||||
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,
|
||||
},
|
||||
{
|
||||
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',
|
||||
uuid: 'test-uuid-2',
|
||||
title: 'How to Learn Online 2',
|
||||
subtitle: 'Org 2',
|
||||
cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-2.png',
|
||||
authoringOrganizations: [
|
||||
{
|
||||
key: 'BUx',
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/36cfd0bb-1d18-4355-ae44-cb946573df3c-1e18515c3e4b.png',
|
||||
name: 'Boston University',
|
||||
},
|
||||
],
|
||||
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,
|
||||
},
|
||||
{
|
||||
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: 'UniversityofLondon',
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/5427ee88-6dba-46ff-8ed8-87b71e3234da-0af1465ab67e.png',
|
||||
name: 'University of London',
|
||||
},
|
||||
],
|
||||
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,
|
||||
},
|
||||
{
|
||||
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: 'HarvardX',
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/44022f13-20df-4666-9111-cede3e5dc5b6-2cc39992c67a.png',
|
||||
name: 'Harvard University',
|
||||
key: 'org-2',
|
||||
logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-2.png',
|
||||
name: 'Org 2',
|
||||
},
|
||||
],
|
||||
courses: [
|
||||
{
|
||||
course: {
|
||||
title: 'Exercising Leadership: Foundational Principles',
|
||||
title: 'How to learn online course 1',
|
||||
topics: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
course: {
|
||||
title: 'Remote Work Revolution for Everyone',
|
||||
title: 'How to learn online course 2',
|
||||
topics: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'Professional Certificate',
|
||||
marketingPath: '/professional-certificate/harvardx-leading-in-a-remote-environment',
|
||||
url: '/test-professional-certificate/how-to-learn-online-2',
|
||||
organizationLogoOverrideUrl: null,
|
||||
organizationShortCodeOverride: '',
|
||||
productSource: {
|
||||
name: 'edX',
|
||||
slug: 'edx',
|
||||
description: '',
|
||||
name: 'company X',
|
||||
slug: 'companyX',
|
||||
},
|
||||
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,
|
||||
},
|
||||
{
|
||||
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: 'UTAustinX',
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/9d38ad58-87fb-4a89-9f23-c8df318112e3-aec8e9e98a5f.png',
|
||||
name: 'The University of Texas at Austin',
|
||||
},
|
||||
],
|
||||
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 default mockedCoursesData;
|
||||
export default mockedProductData;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
.nav-tabs {
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.recommendations-container__card-list {
|
||||
padding-left: 0.0625rem;
|
||||
padding-bottom: 0.125rem;
|
||||
|
||||
@include media-breakpoint-down(xl) {
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
@@ -13,6 +18,7 @@
|
||||
.recommendations-container {
|
||||
padding: 0 1rem;
|
||||
margin: 0 0 1.875rem 0;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
max-width: $max-width-sm + 5 * $grid-gutter-width !important;
|
||||
}
|
||||
@@ -24,25 +30,6 @@
|
||||
@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 {
|
||||
|
||||
Reference in New Issue
Block a user