fix: core mark-up and styling for cross product recommendations container

This commit is contained in:
Jody Bailey
2023-05-15 15:25:33 +02:00
parent 807d9f70b8
commit be1e1bf7d9
6 changed files with 412 additions and 20 deletions

View File

@@ -0,0 +1,78 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Badge,
Card,
Truncate,
Hyperlink,
} from '@edx/paragon';
const ProductCard = ({
title,
subtitle,
headerImage,
schoolLogo,
courseType,
url,
}) => {
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 productType = courseTypeToProductTypeMap[courseType];
return (
<div className="base-card-wrapper">
<Hyperlink destination={url}>
<Card className="base-card light" variant="light">
<Card.ImageCap
src={headerImage}
srcAlt={`header image for ${title}`}
logoSrc={schoolLogo}
logoAlt={`logo for ${subtitle}`}
/>
<Card.Header
className="mt-2"
title={(
<Truncate lines={3} ellipsis="…" className="product-card-title">
{title}
</Truncate>
)}
subtitle={(
<Truncate lines={1} className="product-card-subtitle">
{subtitle}
</Truncate>
)}
/>
<Card.Section>
<div className="product-badge">
<Badge>{productType}</Badge>
</div>
</Card.Section>
</Card>
</Hyperlink>
</div>
);
};
ProductCard.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string.isRequired,
headerImage: PropTypes.string.isRequired,
schoolLogo: PropTypes.string.isRequired,
courseType: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
};
export default ProductCard;

View File

@@ -0,0 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ProductCard from './ProductCard';
import ProductCardHeader from './ProductCardHeader';
const ProductCardContainer = ({ courses }) => {
const courseTypes = [...new Set(courses.map((item) => item.courseType))];
return (
<div className="product-card-container d-flex">
{courses
&& courseTypes.map((type) => (
<div key={type}>
<ProductCardHeader courseType={type} />
<div
className={classNames({
'course-subcontainer': type === 'course',
})}
>
{courses
.filter((course) => course.courseType === type)
.map((item) => (
<ProductCard
key={item.uuid}
url={`https://www.edx.org/${item.prospectusPath}`}
title={item.title}
subtitle={item.owners[0].name}
headerImage={item.image.src}
schoolLogo={item.owners[0].logoImageUrl}
courseType={type}
/>
))}
</div>
</div>
))}
</div>
);
};
ProductCardContainer.propTypes = {
courses: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string,
uuid: PropTypes.string,
title: PropTypes.string,
image: PropTypes.shape({
src: PropTypes.string,
}),
prospectusPath: PropTypes.string,
owners: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string,
name: PropTypes.string,
logoImageUrl: PropTypes.string,
}),
),
activeCourseRun: PropTypes.shape({
key: PropTypes.string,
marketingUrl: PropTypes.string,
}),
courseType: PropTypes.string,
}),
).isRequired,
};
export default ProductCardContainer;

View File

@@ -2,27 +2,25 @@ import React from 'react';
import { Container } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../messages';
import ProductCardHeader from './ProductCardHeader';
import './index.scss';
import messages from '../messages';
import ProductCardContainer from './ProductCardContainer';
import mockCrossProductRecommendations from '../mockData';
const ProductRecommendationsContainer = () => {
const { formatMessage } = useIntl();
const mockRecommendations = ['executive-education', 'bootcamp-2u', 'course'];
const mockRecommendations = mockCrossProductRecommendations.courses;
return (
<div className="bg-light-200">
<Container
size="lg"
className="recommendations-container border-dark-200 pt-sm-5 pt-4.5 pb-2 pb-sm-4.5"
className="recommendations-container pt-sm-5 pt-4.5 pb-2 pb-sm-4.5"
>
<h2>{formatMessage(messages.recommendationsHeading)}</h2>
{/* {mockRecommendations.forEach((recommendation) => (
<ProductCardContainer />
))} */}
{mockRecommendations.map((courseType) => (
<ProductCardHeader courseType={courseType} />
))}
<h2 className="mb-4">
{formatMessage(messages.recommendationsHeading)}
</h2>
<ProductCardContainer courses={mockRecommendations} />
</Container>
</div>
);

View File

@@ -1,8 +1,153 @@
@import "@edx/paragon/scss/core/core";
$horizontal-card-gap: 20px;
$vertical-card-gap: 24px;
$card-height: 332px;
$header-height: 104px;
$card-width: 270px;
.recommendations-container {
border: solid
border: solid;
}
.base-card {
height: $card-height;
width: $card-width !important;
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;
}
}
.product-card-title {
font: {
size: 1.125rem;
weight: 700;
}
line-height: 24px ;
}
.product-card-subtitle {
font: {
size: 0.875rem;
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-link:hover {
text-decoration: none;
}
.base-card:hover {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15), 0 0.125rem 0.5rem rgba(0, 0, 0, 0.15);
}
.base-card-link .base-card {
display: flex;
}
.product-card-container {
gap: $vertical-card-gap $horizontal-card-gap;
margin: 0 (-$horizontal-card-gap);
padding: 0 $horizontal-card-gap;
.course-subcontainer {
display: flex;
gap: $vertical-card-gap $horizontal-card-gap;
}
// .base-card-wrapper {
// flex: 0 1 calc(100% - #{$horizontal-card-gap});
// @include media-breakpoint-up(sm) {
// flex: 0 1 calc(50% - #{$horizontal-card-gap});
// }
// @include media-breakpoint-up(md) {
// flex: 0 1 calc(33.333% - #{$horizontal-card-gap});
// }
// @include media-breakpoint-up(xl) {
// flex: 0 1 calc(25% - #{$horizontal-card-gap});
// }
// }
}

View File

@@ -0,0 +1,100 @@
const mockCrossProductRecommendations = {
courses: [
{
key: 'HarvardX+CRS',
uuid: 'fee0ed87-a122-46a8-b233-3e5c75c755d1',
title: 'CRISPR: Gene-editing Applications',
image: {
src: 'https://prod-discovery.edx-cdn.org/media/course/image/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg',
},
prospectusPath:
'course/harvard-vpal-crispr-gene-editing-applications-online-short-course',
owners: [
{
key: 'HarvardX',
name: 'Harvard University',
logoImageUrl:
'http://localhost:18381/media/organization/logos/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png',
},
],
activeCourseRun: {
key: 'course-v1:HarvardX+CRS+1T2023',
marketingUrl:
'course/crispr-gene-editing-applications-course-v1-harvardx-crs-1t2023?utm_source=discovery_worker&utm_medium=affiliate_partner',
},
courseType: 'executive-education-2u',
},
{
key: 'AdelaideX+BC24CYB',
uuid: 'dacb53da-35c7-4c6b-b8d0-d7d8c0c78cef',
title: 'Cybersecurity Boot Camp',
image: {
src: 'https://prod-discovery.edx-cdn.org/media/course/image/fbb62b99-9f85-4563-a67c-34ac827560bd-e3e6263b98fd.small.png',
},
prospectusPath:
'course/the-university-of-adelaide-cybersecurity-boot-camp',
owners: [
{
key: 'AdelaideX',
name: 'University of Adelaide',
logoImageUrl:
'http://localhost:18381/media/organization/logos/51d054b4-9589-4376-9e9f-15656f3c8d0e-f10fb90c3ff1.png',
},
],
activeCourseRun: {
key: 'course-v1:AdelaideX+BC24CYB+1T2023',
marketingUrl:
'course/cybersecurity-boot-camp-course-v1-adelaidex-bc24cyb-1t2023?utm_source=discovery_worker&utm_medium=affiliate_partner',
},
courseType: 'bootcamp-2u',
},
{
key: 'AA+AA101',
uuid: '1e2cae8c-1c67-4067-a3c0-360543e6a9b8',
title: 'Data Analytics for Business',
image: {
src: 'https://prod-discovery.edx-cdn.org/media/course/image/1e2cae8c-1c67-4067-a3c0-360543e6a9b8-50beebb61f2e.small.png',
},
prospectusPath: '/course/data-analytics-for-business',
owners: [
{
key: 'GTx',
name: 'The Georgia Institute of Technology',
logoImageUrl:
'https://prod-discovery.edx-cdn.org/organization/logos/8537d31f-01b4-40fd-b652-e17b38eefe41-7956b2a3cd04.png',
},
],
activeCourseRun: {
key: 'course-v1:GTx+MGT6203x+2T2023',
marketingUrl:
'https://www.edx.org/course/data-analytics-for-business-course-v1gtxmgt6203x2t2023?utm_source=prospectus_worker&utm_medium=affiliate_partner',
},
courseType: 'course',
},
{
key: 'AA+AA101',
uuid: '876a65e7-425b-437b-bced-bdf8059fec81',
title: 'Western and Chinese Art: Masters and Classics',
image: {
src: 'https://prod-discovery.edx-cdn.org/media/course/image/876a65e7-425b-437b-bced-bdf8059fec81.small.jpg',
},
prospectusPath: '/course/western-and-chinese-art-masters-and-classics',
owners: [
{
key: 'TsinghuaX',
name: 'Tsinghua University',
logoImageUrl:
'https://prod-discovery.edx-cdn.org/organization/logos/b5714409-b5f4-4c9d-9348-b0fecbaaddd6-780fbb6c72c7.png',
},
],
activeCourseRun: {
key: 'course-v1:TsinghuaX+00691153.x+1T2015',
marketingUrl:
'https://www.edx.org/course/western-chinese-art-masters-classics-tsinghuax-00691153x?utm_source=prospectus_worker&utm_medium=affiliate_partner',
},
courseType: 'course',
},
],
};
export default mockCrossProductRecommendations;

View File

@@ -4,6 +4,7 @@ import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
import LoadingView from './LoadingView';
import LoadedView from './LoadedView';
import hooks from './hooks';
import recommendedCoursesData from "../RecommendationsPanel/mockData";
export const RecommendationsPanel = () => {
const {
@@ -17,14 +18,17 @@ export const RecommendationsPanel = () => {
if (isLoading) {
return (<LoadingView />);
}
if (isLoaded && courses.length > 0) {
return (
<LoadedView courses={courses} isControl={isControl} />
);
}
if (isFailed) {
return (<LookingForChallengeWidget />);
}
const newCourses = recommendedCoursesData.courses;
// if (newCourses.length > 0) {
// return (
// <LoadedView courses={newCourses} isControl={false} />
// );
// }
// if (isFailed) {
// return (<LookingForChallengeWidget />);
// }
// default fallback
return (<LookingForChallengeWidget />);
};