Skip to content

Commit

Permalink
feat: adding program card functionality/formatting (#178)
Browse files Browse the repository at this point in the history
* feat: adding program card

* fix: failing tests with config changes

* fix: lint changes

* fix: PR requests
  • Loading branch information
kiram15 committed Jan 6, 2022
1 parent 6ad8b53 commit b2ac9d3
Show file tree
Hide file tree
Showing 17 changed files with 281 additions and 28 deletions.
2 changes: 1 addition & 1 deletion src/components/PageWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const DATA_TEST_ID = 'enterprise-catalogs-content';

// eslint-disable-next-line react/prop-types
const PageWrapper = ({ children, className }) => (
<Container size="lg" className={className}>
<Container className={className}>
<Helmet title={PAGE_TITLE} />
<div data-testid={DATA_TEST_ID}>
{children}
Expand Down
16 changes: 12 additions & 4 deletions src/components/catalogPage/CatalogPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ import Hero from '../hero/Hero';
import messages from './CatalogPage.messages';
import CatalogSelectionDeck from '../catalogSelectionDeck/CatalogSelectionDeck';
import {
AVAILABILITY_REFINEMENT, AVAILABILITY_REFINEMENT_DEFAULTS, QUERY_TITLE_REFINEMENT,
HIDE_CARDS_REFINEMENT, TRACKING_APP_NAME,
AVAILABILITY_REFINEMENT, AVAILABILITY_REFINEMENT_DEFAULTS, CONTENT_TYPE_REFINEMENT,
QUERY_TITLE_REFINEMENT, HIDE_CARDS_REFINEMENT, TRACKING_APP_NAME,
} from '../../constants';

const contentType = {
attribute: 'content_type',
title: 'Type',
};
SEARCH_FACET_FILTERS.push(contentType);

const CatalogPage = ({ intl }) => {
const config = getConfig();
// Default routing:
Expand All @@ -24,11 +30,13 @@ const CatalogPage = ({ intl }) => {
const loadedSearchParams = new URLSearchParams(window.location.search);
let reloadPage = false;
let hideCards = false;
if (config.EDX_ENTERPRISE_ALACARTE_TITLE && (!loadedSearchParams.get(QUERY_TITLE_REFINEMENT))) {
if (config.EDX_ENTERPRISE_ALACARTE_TITLE
&& (!loadedSearchParams.get(CONTENT_TYPE_REFINEMENT))
&& (!loadedSearchParams.get(QUERY_TITLE_REFINEMENT))) {
loadedSearchParams.set(QUERY_TITLE_REFINEMENT, config.EDX_ENTERPRISE_ALACARTE_TITLE);
reloadPage = true;
}
if ((!loadedSearchParams.get(AVAILABILITY_REFINEMENT))) {
if ((!loadedSearchParams.get(AVAILABILITY_REFINEMENT) && (!loadedSearchParams.get(CONTENT_TYPE_REFINEMENT)))) {
AVAILABILITY_REFINEMENT_DEFAULTS.map(a => loadedSearchParams.append(AVAILABILITY_REFINEMENT, a));
reloadPage = true;
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/catalogSearchResults/CatalogSearchResults.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable camelcase */
import React, {
useContext,
useMemo,
Expand Down Expand Up @@ -26,6 +25,7 @@ import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/
import { GridView, ListView } from '@edx/paragon/icons';

import CourseCard from '../courseCard/CourseCard';

import CatalogCourseInfoModal from '../catalogCourseInfoModal/CatalogCourseInfoModal';
import DownloadCsvButton from './buttons/downloadCsvButton/DownloadCsvButton';
import messages from './CatalogSearchResults.messages';
Expand Down Expand Up @@ -306,7 +306,7 @@ export const BaseCatalogSearchResults = ({
xs: 12,
sm: 6,
md: 4,
lg: 3,
lg: 4,
xl: 3,
}}
CardComponent={(props) => <CourseCard {...props} onClick={cardClicked} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const CatalogSelectionDeck = ({ intl, title, hide }) => {
const config = getConfig();
return (
<section className="catalog-selection-deck" style={{ display: hide ? 'none' : 'block' }}>
<Container size="lg">
<Container className="page-width">
<h2>{title}</h2>
<CardDeck>
<CatalogSelectionCard
Expand Down
2 changes: 1 addition & 1 deletion src/components/catalogs/EnterpriseCatalogs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function EnterpriseCatalogs() {

return (
<>
<PageWrapper className="enterprise-catalogs">
<PageWrapper className="enterprise-catalogs page-width">
<section>
<FormattedMessage
id="catalogs.enterpriseCatalogs.header"
Expand Down
27 changes: 20 additions & 7 deletions src/components/catalogs/styles/_enterprise_catalogs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@
line-height: 24px;
}

// cc - course card
.cc-partner-logo {
.cards-partner-logo {
left: 5%;
height: auto;
object-fit: cover;
Expand All @@ -93,22 +92,36 @@
box-shadow: 0 1px 1px rgba(128, 128, 128, 0.1), 0 1px 1px rgba(128, 128, 128, 0.1), 0 2px 2px rgba(128, 128, 128, 0.1),
0 2px 2px rgba(128, 128, 128, 0.1);
}
.cc-course-image {
height: 100px;

.cards-course-image {
height: 100px;
object-fit: cover;
width: auto;
}

.cc-title {
.cards-title {
margin: 25px 16px 25px 16px;
}
.cc-spacing {

.cards-spacing {
flex-grow: 3;
}
.cc-body {

.cards-body {
margin: 25px 16px 25px 16px;
}

.program-card {
background: $primary-500;
border: 0px;
color: $white;
}

.program-title {
font-weight: 700;
color: $white;
}

.banner {
display: flex;
margin: 38px 0px 38px 0px;
Expand Down
10 changes: 5 additions & 5 deletions src/components/courseCard/CourseCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ const CourseCard = ({
return (
<Card className={className} tabIndex="0" onClick={() => onClick(original)}>
<Card.Img
className="cc-course-image"
className="cards-course-image"
variant="top"
src={card_image_url}
alt={title}
/>
<Image className="mr-2 cc-partner-logo" src={partners[0].logo_image_url} rounded />
<div className="cc-title">
<Image className="mr-2 cards-partner-logo" src={partners[0].logo_image_url} rounded />
<div className="cards-title">
<p className="h4">{title}</p>
<p className="small">{ partners[0].name }</p>
</div>
<span className="cc-spacing" />
<div className="cc-body">
<span className="cards-spacing" />
<div className="cards-body">
<p className="x-small mb-3">{ priceText }{availability[0]}</p>

<div style={{ maxWidth: '400vw' }}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/hero/Hero.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const Hero = ({ intl, text, highlight }) => {

return (
<section className="hero">
<Container size="lg" className="hero__content">
<Container className="page-width hero__content">
<h1 className="display-1"><Highlighted text={text} highlight={highlight} /></h1>
<div>
<Desktop>
Expand Down
106 changes: 106 additions & 0 deletions src/components/programCard/ProgramCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* eslint-disable camelcase */
// variables taken from algolia not in camelcase
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import {
Badge,
Image,
Icon,
Card,
} from '@edx/paragon';
import { Program } from '@edx/paragon/icons';

import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './ProgramCard.messages';

function makePlural(num, string) {
if (num > 1) { return (`${num} ${string}s`); }
return (`${num} ${string}`);
}

const ProgramCard = ({
intl, onClick, className, original,
}) => {
const {
title,
card_image_url,
course_keys,
partners,
enterprise_catalog_query_titles,
program_type,
} = original;
return (
<Card className={className} tabIndex="0" onClick={() => onClick(original)}>
<Card.Img
className="cards-course-image"
variant="top"
src={card_image_url}
alt={title}
/>
{(partners.length !== 0) && <Image className="mr-2 cards-partner-logo" src={partners[0].logo_image_url} rounded />}
<div className="cards-title">
<p className="program-title">{title}</p>
{(partners.length !== 0) && <p className="small">{ partners[0].name }</p>}
</div>
<span className="cards-spacing" />

<div className="cards-body">
<div className="d-flex">
<Badge
variant="light"
className={classNames(
'program d-flex justify-content-center align-items-center text-primary-500', 'mb-2',
)}
>
<Icon src={Program} className="badge-icon" />
<span className="badge-text"> {program_type} </span>
</Badge>
</div>
<p className="x-small mb-2 mt-2">{makePlural(course_keys.length, 'Course')}</p>
{enterprise_catalog_query_titles && (
<div style={{ maxWidth: '400vw' }}>
{
enterprise_catalog_query_titles.includes(process.env.EDX_ENTERPRISE_ALACARTE_TITLE)
&& <Badge variant="info" className="ml-0 bright padded-catalog">{intl.formatMessage(messages['ProgramCard.aLaCarteBadge'])}</Badge>
}
{
enterprise_catalog_query_titles.includes(process.env.EDX_FOR_BUSINESS_TITLE)
&& <Badge variant="secondary" className="padded-catalog">{intl.formatMessage(messages['ProgramCard.businessBadge'])}</Badge>
}
{
enterprise_catalog_query_titles.includes(process.env.EDX_FOR_ONLINE_EDU_TITLE)
&& (
<Badge className="padded-catalog" variant="light">
{intl.formatMessage(messages['ProgramCard.educationBadge'])}
</Badge>
)
}
</div>
)}
</div>
</Card>
);
};

ProgramCard.defaultProps = {
className: '',
onClick: () => {},
};

ProgramCard.propTypes = {
intl: intlShape.isRequired,
className: PropTypes.string,
onClick: PropTypes.func,
original: PropTypes.shape({
title: PropTypes.string,
card_image_url: PropTypes.string,
course_keys: PropTypes.arrayOf(PropTypes.string),
partners: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string, logo_image_url: PropTypes.string })),
enterprise_catalog_query_titles: PropTypes.arrayOf(PropTypes.string),
program_type: PropTypes.string,
}).isRequired,
};

export default injectIntl(ProgramCard);
31 changes: 31 additions & 0 deletions src/components/programCard/ProgramCard.messages.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineMessages } from '@edx/frontend-platform/i18n';

const messages = defineMessages({
'ProgramCard.relatedSkillsHeading': {
id: 'ProgramCard.relatedSkillsHeading',
defaultMessage: 'Related skills',
description: 'Heading of related skills section',
},
'ProgramCard.aLaCarteBadge': {
id: 'ProgramCard.aLaCarteBadge',
defaultMessage: 'A la carte',
description: 'Badge text for the `A La Carte` catalog badge.',
},
'ProgramCard.businessBadge': {
id: 'ProgramCard.businessBadge',
defaultMessage: 'Business',
description: 'Badge text for the `Business` catalog badge.',
},
'ProgramCard.educationBadge': {
id: 'ProgramCard.educationBadge',
defaultMessage: 'Education',
description: 'Badge text for the `Education` catalog badge.',
},
'ProgramCard.priceNotAvailable': {
id: 'ProgramCard.priceNotAvailable',
defaultMessage: ' Not Available',
description: 'When a course price is not available, notify learners that there is no data available to display.',
},
});

export default messages;
33 changes: 33 additions & 0 deletions src/components/programCard/ProgramCard.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';

import { IntlProvider } from '@edx/frontend-platform/i18n';
import ProgramCard from './ProgramCard';

const originalData = {
title: 'Program Title',
card_image_url: '',
course_keys: ['edx+123', 'edx-321'],
partners: [{ logo_image_url: '', name: 'Course Provider' }],
program_type: 'Professional Certificate',
enterprise_catalog_query_titles: [],
};

const defaultProps = {
original: originalData,
};

describe('Program card works as expected', () => {
test('card renders as expected', () => {
render(
<IntlProvider locale="en">
<ProgramCard {...defaultProps} />
</IntlProvider>,
);
expect(screen.queryByText(defaultProps.original.title)).toBeInTheDocument();
expect(screen.queryByText(defaultProps.original.partners[0].name)).toBeInTheDocument();
expect(screen.queryByText(defaultProps.original.program_type)).toBeInTheDocument();
expect(screen.queryByText('2 Courses')).toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion src/components/subheader/subheader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Subheader = ({
title, children,
}) => (
<section>
<Container className="subtitle" size="lg">
<Container className="page-width subtitle">
<div className="subtitle__text">
{title && <h2 className="subtitle__title">{title}</h2>}
<div>{children}</div>
Expand Down
2 changes: 2 additions & 0 deletions src/config/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const FEATURE_ENABLE_PROGRAMS = 'ENABLE_PROGRAMS';
export const FEATURE_PROGRAM_TYPE_FACET = 'ENABLE_PROGRAM_TYPE_FACET';
17 changes: 17 additions & 0 deletions src/config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import qs from 'query-string';
import {
FEATURE_ENABLE_PROGRAMS,
FEATURE_PROGRAM_TYPE_FACET,
} from './constants';

const hasFeatureFlagEnabled = (featureFlag) => {
const { features } = qs.parse(window.location.search);
return features && features.split(',').includes(featureFlag);
};

const features = {
ENABLE_PROGRAMS: (process.env.FEATURE_ENABLE_PROGRAMS === 'true') || hasFeatureFlagEnabled(FEATURE_ENABLE_PROGRAMS),
PROGRAM_TYPE_FACET: (process.env.FEATURE_PROGRAM_TYPE_FACET === 'true') || hasFeatureFlagEnabled(FEATURE_PROGRAM_TYPE_FACET),
};

export default features;

0 comments on commit b2ac9d3

Please sign in to comment.