fix: core mark-up and styling for cross product recommendations container
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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});
|
||||
// }
|
||||
// }
|
||||
}
|
||||
100
src/widgets/ProductRecommendations/mockData.js
Normal file
100
src/widgets/ProductRecommendations/mockData.js
Normal 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;
|
||||
@@ -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 />);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user