feat: [VAN-1291] add recommendation page (#743)
This commit is contained in:
1
.env
1
.env
@@ -25,6 +25,7 @@ DISABLE_ENTERPRISE_LOGIN=''
|
||||
ENABLE_COOKIE_POLICY_BANNER=''
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
||||
ENABLE_PERSONALIZED_RECOMMENDATIONS=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS=''
|
||||
# ***** Zendesk related keys *****
|
||||
|
||||
@@ -10,11 +10,18 @@ import {
|
||||
} from './common-components';
|
||||
import configureStore from './data/configureStore';
|
||||
import {
|
||||
AUTHN_PROGRESSIVE_PROFILING, LOGIN_PAGE, PAGE_NOT_FOUND, PASSWORD_RESET_CONFIRM, REGISTER_PAGE, RESET_PAGE,
|
||||
AUTHN_PROGRESSIVE_PROFILING,
|
||||
LOGIN_PAGE,
|
||||
PAGE_NOT_FOUND,
|
||||
PASSWORD_RESET_CONFIRM,
|
||||
RECOMMENDATIONS,
|
||||
REGISTER_PAGE,
|
||||
RESET_PAGE,
|
||||
} from './data/constants';
|
||||
import { updatePathWithQueryParams } from './data/utils';
|
||||
import ForgotPasswordPage from './forgot-password';
|
||||
import { ProgressiveProfiling } from './progressive-profiling';
|
||||
import RecommendationsPage from './recommendations';
|
||||
import ResetPasswordPage from './reset-password';
|
||||
import './index.scss';
|
||||
|
||||
@@ -34,6 +41,7 @@ const MainApp = () => (
|
||||
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
|
||||
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
|
||||
<Route exact path={AUTHN_PROGRESSIVE_PROFILING} component={ProgressiveProfiling} />
|
||||
<Route exact path={RECOMMENDATIONS} component={RecommendationsPage} />
|
||||
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
|
||||
<Route path="*">
|
||||
<Redirect to={PAGE_NOT_FOUND} />
|
||||
|
||||
@@ -4,12 +4,17 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { AUTHN_PROGRESSIVE_PROFILING } from '../data/constants';
|
||||
import { AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS } from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
|
||||
function RedirectLogistration(props) {
|
||||
const {
|
||||
finishAuthUrl, redirectUrl, redirectToProgressiveProfilingPage, success, optionalFields,
|
||||
finishAuthUrl,
|
||||
redirectUrl,
|
||||
redirectToProgressiveProfilingPage,
|
||||
success,
|
||||
optionalFields,
|
||||
redirectToRecommendationsPage,
|
||||
} = props;
|
||||
let finalRedirectUrl = '';
|
||||
|
||||
@@ -41,6 +46,20 @@ function RedirectLogistration(props) {
|
||||
);
|
||||
}
|
||||
|
||||
// Redirect to Recommendation page
|
||||
if (redirectToRecommendationsPage) {
|
||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||
return (
|
||||
<Redirect to={{
|
||||
pathname: RECOMMENDATIONS,
|
||||
state: {
|
||||
registrationResult,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
window.location.href = finalRedirectUrl;
|
||||
}
|
||||
return <></>;
|
||||
@@ -52,6 +71,7 @@ RedirectLogistration.defaultProps = {
|
||||
redirectUrl: '',
|
||||
redirectToProgressiveProfilingPage: false,
|
||||
optionalFields: {},
|
||||
redirectToRecommendationsPage: false,
|
||||
};
|
||||
|
||||
RedirectLogistration.propTypes = {
|
||||
@@ -60,6 +80,7 @@ RedirectLogistration.propTypes = {
|
||||
redirectUrl: PropTypes.string,
|
||||
redirectToProgressiveProfilingPage: PropTypes.bool,
|
||||
optionalFields: PropTypes.shape({}),
|
||||
redirectToRecommendationsPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default RedirectLogistration;
|
||||
|
||||
@@ -10,6 +10,7 @@ const configuration = {
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
|
||||
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
|
||||
ENABLE_PERSONALIZED_RECOMMENDATIONS: process.env.ENABLE_PERSONALIZED_RECOMMENDATIONS || false,
|
||||
// Links
|
||||
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: process.env.AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK || null,
|
||||
|
||||
@@ -4,6 +4,7 @@ export const REGISTER_PAGE = '/register';
|
||||
export const RESET_PAGE = '/reset';
|
||||
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
|
||||
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
||||
export const RECOMMENDATIONS = '/recommendations';
|
||||
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
||||
export const PAGE_NOT_FOUND = '/notfound';
|
||||
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
||||
|
||||
@@ -49,8 +49,8 @@ export const updatePathWithQueryParams = (path) => {
|
||||
return `${path}${queryParams}`;
|
||||
};
|
||||
|
||||
export const getAllPossibleQueryParams = () => {
|
||||
const urlParams = QueryString.parse(window.location.search);
|
||||
export const getAllPossibleQueryParams = (locationURl = null) => {
|
||||
const urlParams = QueryString.parse(locationURl || window.location.search);
|
||||
const params = {};
|
||||
Object.entries(urlParams).forEach(([key, value]) => {
|
||||
if (AUTH_PARAMS.indexOf(key) > -1) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { RedirectLogistration } from '../common-components';
|
||||
import {
|
||||
DEFAULT_REDIRECT_URL, DEFAULT_STATE, FAILURE_STATE,
|
||||
} from '../data/constants';
|
||||
import { getAllPossibleQueryParams } from '../data/utils';
|
||||
import FormFieldRenderer from '../field-renderer';
|
||||
import { saveUserProfile } from './data/actions';
|
||||
import { welcomePageSelector } from './data/selectors';
|
||||
@@ -35,12 +36,15 @@ import ProgressiveProfilingPageModal from './ProgressiveProfilingPageModal';
|
||||
|
||||
const ProgressiveProfiling = (props) => {
|
||||
const {
|
||||
formRenderState, intl, submitState, showError,
|
||||
formRenderState, intl, submitState, showError, location,
|
||||
} = props;
|
||||
const enablePersonalizedRecommendations = getConfig().ENABLE_PERSONALIZED_RECOMMENDATIONS;
|
||||
const registrationResponse = location.state?.registrationResult;
|
||||
const [ready, setReady] = useState(false);
|
||||
const [registrationResult, setRegistrationResult] = useState({ redirectUrl: '' });
|
||||
const [values, setValues] = useState({});
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [showRecommendationsPage, setShowRecommendationsPage] = useState(false);
|
||||
|
||||
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
|
||||
@@ -54,13 +58,23 @@ const ProgressiveProfiling = (props) => {
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
if (props.location.state && props.location.state.registrationResult) {
|
||||
setRegistrationResult(props.location.state.registrationResult);
|
||||
let userEnrollmentAction = false;
|
||||
if (registrationResponse) {
|
||||
setRegistrationResult(registrationResponse);
|
||||
sendPageEvent('login_and_registration', 'welcome');
|
||||
}
|
||||
}, [DASHBOARD_URL, props.location.state]);
|
||||
|
||||
if (!props.location.state || !props.location.state.registrationResult || formRenderState === FAILURE_STATE) {
|
||||
const queryParams = getAllPossibleQueryParams(registrationResponse.redirectUrl);
|
||||
if ('enrollment_action' in queryParams) {
|
||||
userEnrollmentAction = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (enablePersonalizedRecommendations && !userEnrollmentAction) {
|
||||
setShowRecommendationsPage(true);
|
||||
}
|
||||
}, [DASHBOARD_URL, enablePersonalizedRecommendations, registrationResponse]);
|
||||
|
||||
if (!location.state || !location.state.registrationResult || formRenderState === FAILURE_STATE) {
|
||||
global.location.assign(DASHBOARD_URL);
|
||||
return null;
|
||||
}
|
||||
@@ -69,11 +83,12 @@ const ProgressiveProfiling = (props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const optionalFields = props.location.state.optionalFields.fields;
|
||||
const extendedProfile = props.location.state.optionalFields.extended_profile;
|
||||
const optionalFields = location.state.optionalFields.fields;
|
||||
const extendedProfile = location.state.optionalFields.extended_profile;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
window.history.replaceState(props.location.state, null, '');
|
||||
window.history.replaceState(location.state, null, '');
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
const payload = { ...values, extendedProfile: [] };
|
||||
if (Object.keys(extendedProfile).length > 0) {
|
||||
@@ -137,6 +152,7 @@ const ProgressiveProfiling = (props) => {
|
||||
<RedirectLogistration
|
||||
success
|
||||
redirectUrl={registrationResult.redirectUrl}
|
||||
redirectToRecommendationsPage={showRecommendationsPage}
|
||||
/>
|
||||
) : null}
|
||||
<div className="mw-xs pp-page-content">
|
||||
@@ -171,7 +187,7 @@ const ProgressiveProfiling = (props) => {
|
||||
className="login-button-width"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['optional.fields.submit.button']),
|
||||
default: showRecommendationsPage ? intl.formatMessage(messages['optional.fields.next.button']) : intl.formatMessage(messages['optional.fields.submit.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
|
||||
@@ -26,6 +26,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Skip for now',
|
||||
description: 'Skip button text',
|
||||
},
|
||||
'optional.fields.next.button': {
|
||||
id: 'optional.fields.next.button',
|
||||
defaultMessage: 'Next',
|
||||
description: 'Next button text',
|
||||
},
|
||||
// modal dialog box
|
||||
'continue.to.platform': {
|
||||
id: 'continue.to.platform',
|
||||
|
||||
59
src/recommendations/RecommendationCard.jsx
Normal file
59
src/recommendations/RecommendationCard.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Card, Hyperlink } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const RecommendationCard = (props) => {
|
||||
const { recommendation } = 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);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mr-4 recommendation-card">
|
||||
<Hyperlink destination={recommendation.marketingUrl} target="_blank" showLaunchIcon={false}>
|
||||
<Card isClickable>
|
||||
<Card.ImageCap
|
||||
src={recommendation.cardImageUrl}
|
||||
srcAlt="Card image"
|
||||
logoSrc={showPartnerLogo && recommendation.owners[0].logoImageUrl}
|
||||
logoAlt="Card logo"
|
||||
/>
|
||||
<Card.Header
|
||||
title={recommendation.title}
|
||||
subtitle={getOwners()}
|
||||
/>
|
||||
<Card.Section />
|
||||
<Card.Footer textElement={<small className="footer-text">Course</small>} />
|
||||
</Card>
|
||||
</Hyperlink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RecommendationCard.propTypes = {
|
||||
recommendation: PropTypes.shape({
|
||||
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,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(RecommendationCard);
|
||||
50
src/recommendations/RecommendationsList.jsx
Normal file
50
src/recommendations/RecommendationsList.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Container } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import RecommendationCard from './RecommendationCard';
|
||||
|
||||
const RecommendationsList = (props) => {
|
||||
const { title, recommendations } = props;
|
||||
|
||||
return (
|
||||
<Container size="lg" className="recommendations-container">
|
||||
<h2 className="text-sm-center mb-4 text-left recommendations-heading">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="d-flex card-list">
|
||||
{
|
||||
recommendations.map((recommendation) => (
|
||||
<RecommendationCard
|
||||
key={recommendation.activeRunKey}
|
||||
recommendation={recommendation}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
RecommendationsList.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
recommendations: PropTypes.arrayOf(PropTypes.shape({
|
||||
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,
|
||||
})),
|
||||
};
|
||||
|
||||
RecommendationsList.defaultProps = {
|
||||
recommendations: [],
|
||||
};
|
||||
|
||||
export default injectIntl(RecommendationsList);
|
||||
143
src/recommendations/RecommendationsPage.jsx
Normal file
143
src/recommendations/RecommendationsPage.jsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Hyperlink, Image, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { DEFAULT_REDIRECT_URL } from '../data/constants';
|
||||
import messages from './messages';
|
||||
import RecommendationsList from './RecommendationsList';
|
||||
|
||||
const recommendationData = [
|
||||
{
|
||||
activeRunKey: 'course-v1:MITx+6.86x+1T2023',
|
||||
cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/course/image/4c70ad9b-9602-49af-bf00-83fa4bf47708-dc4566d15250.jpg',
|
||||
marketingUrl:
|
||||
'https://www.edx.org/course/machine-learning-with-python-from-linear-models-to',
|
||||
objectId: 'course-4c70ad9b-9602-49af-bf00-83fa4bf47708',
|
||||
owners: [
|
||||
{
|
||||
key: 'MITx',
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/2a73d2ce-c34a-4e08-8223-83bca9d2f01d-2cc8854c6fee.png',
|
||||
name: 'Massachusetts Institute of Technology',
|
||||
},
|
||||
],
|
||||
title: 'Machine Learning with Python: from Linear Models to Deep Learning',
|
||||
},
|
||||
{
|
||||
activeRunKey: 'course-v1:MITx+6.86x+1T2023',
|
||||
cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/course/image/4c70ad9b-9602-49af-bf00-83fa4bf47708-dc4566d15250.jpg',
|
||||
marketingUrl:
|
||||
'https://www.edx.org/course/machine-learning-with-python-from-linear-models-to',
|
||||
objectId: 'course-4c70ad9b-9602-49af-bf00-83fa4bf47708',
|
||||
owners: [
|
||||
{
|
||||
key: 'MITx',
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/2a73d2ce-c34a-4e08-8223-83bca9d2f01d-2cc8854c6fee.png',
|
||||
name: 'Massachusetts Institute of Technology',
|
||||
},
|
||||
{
|
||||
key: 'MITx',
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/2a73d2ce-c34a-4e08-8223-83bca9d2f01d-2cc8854c6fee.png',
|
||||
name: 'Massachusetts Institute of Technology',
|
||||
},
|
||||
],
|
||||
title: 'Machine Learning with Python: from Linear Models to Deep Learning',
|
||||
},
|
||||
{
|
||||
activeRunKey: 'course-v1:MITx+6.86x+1T2023',
|
||||
cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/course/image/4c70ad9b-9602-49af-bf00-83fa4bf47708-dc4566d15250.jpg',
|
||||
marketingUrl:
|
||||
'https://www.edx.org/course/machine-learning-with-python-from-linear-models-to',
|
||||
objectId: 'course-4c70ad9b-9602-49af-bf00-83fa4bf47708',
|
||||
owners: [
|
||||
{
|
||||
key: 'MITx',
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/2a73d2ce-c34a-4e08-8223-83bca9d2f01d-2cc8854c6fee.png',
|
||||
name: 'Massachusetts Institute of Technology',
|
||||
},
|
||||
],
|
||||
title: 'Machine Learning with Python: from Linear Models to Deep Learning',
|
||||
},
|
||||
{
|
||||
activeRunKey: 'course-v1:MITx+6.86x+1T2023',
|
||||
cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/course/image/4c70ad9b-9602-49af-bf00-83fa4bf47708-dc4566d15250.jpg',
|
||||
marketingUrl:
|
||||
'https://www.edx.org/course/machine-learning-with-python-from-linear-models-to',
|
||||
objectId: 'course-4c70ad9b-9602-49af-bf00-83fa4bf47708',
|
||||
owners: [
|
||||
{
|
||||
key: 'MITx',
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/2a73d2ce-c34a-4e08-8223-83bca9d2f01d-2cc8854c6fee.png',
|
||||
name: 'Massachusetts Institute of Technology',
|
||||
},
|
||||
],
|
||||
title: 'Machine Learning with Python: from Linear Models to Deep Learning',
|
||||
},
|
||||
];
|
||||
|
||||
const RecommendationsPage = (props) => {
|
||||
const { intl, location } = props;
|
||||
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
const registrationResponse = location.state?.registrationResult;
|
||||
|
||||
if (!registrationResponse) {
|
||||
global.location.assign(DASHBOARD_URL);
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSkip = (e) => {
|
||||
e.preventDefault();
|
||||
window.history.replaceState(location.state, null, '');
|
||||
if (registrationResponse) {
|
||||
window.location.href = registrationResponse.redirectUrl;
|
||||
} else {
|
||||
window.location.href = DASHBOARD_URL;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column vh-100">
|
||||
<div className="mb-2">
|
||||
<div className="col-md-12 small-screen-top-stripe medium-screen-top-stripe extra-large-screen-top-stripe" />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
||||
</Hyperlink>
|
||||
</div>
|
||||
<div className="d-flex flex-column align-items-center justify-content-center flex-grow-1 p-1">
|
||||
<RecommendationsList
|
||||
title={intl.formatMessage(messages['recommendation.page.heading'])}
|
||||
recommendations={recommendationData}
|
||||
/>
|
||||
<div className="text-center">
|
||||
<StatefulButton
|
||||
className=" font-weight-500"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['recommendation.skip.button']),
|
||||
}}
|
||||
onClick={handleSkip}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RecommendationsPage.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
location: PropTypes.shape({
|
||||
state: PropTypes.object,
|
||||
}),
|
||||
|
||||
};
|
||||
|
||||
RecommendationsPage.defaultProps = {
|
||||
location: { state: {} },
|
||||
};
|
||||
|
||||
export default injectIntl(RecommendationsPage);
|
||||
1
src/recommendations/index.js
Normal file
1
src/recommendations/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './RecommendationsPage';
|
||||
21
src/recommendations/messages.js
Normal file
21
src/recommendations/messages.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'recommendation.page.title': {
|
||||
id: 'recommendation.page.title',
|
||||
defaultMessage: 'Recommendations| {siteName}',
|
||||
description: 'recommendation page title',
|
||||
},
|
||||
'recommendation.page.heading': {
|
||||
id: 'recommendation.page.heading',
|
||||
defaultMessage: 'We have a few recommendations to get you started.',
|
||||
description: 'recommendation page heading',
|
||||
},
|
||||
'recommendation.skip.button': {
|
||||
id: 'recommendation.skip.button',
|
||||
defaultMessage: 'Skip for now',
|
||||
description: 'Skip button text',
|
||||
},
|
||||
|
||||
});
|
||||
export default messages;
|
||||
66
src/sass/_recommendations_page.scss
Normal file
66
src/sass/_recommendations_page.scss
Normal file
@@ -0,0 +1,66 @@
|
||||
.card-list {
|
||||
padding-left: 0.0625rem;
|
||||
padding-bottom: 0.125;
|
||||
@include media-breakpoint-down(xl) {
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
.recommendations-heading {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
max-width: $max-width-md + $grid-gutter-width !important;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xxl) {
|
||||
max-width: $max-width-lg + $grid-gutter-width !important;
|
||||
}
|
||||
}
|
||||
|
||||
.recommendation-card {
|
||||
.pgn__hyperlink {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pgn__card {
|
||||
width: 281px;
|
||||
height: 332px;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.pgn__card-image-cap {
|
||||
height: 6.5rem;
|
||||
}
|
||||
|
||||
.pgn__card-header-title-md {
|
||||
font-weight: 700;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.pgn__card-header-subtitle-md{
|
||||
font-weight: 400;
|
||||
font-size:0.875rem;
|
||||
line-height: 1.5rem;
|
||||
color: $gray-700;
|
||||
}
|
||||
.pgn__card-footer {
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
padding-bottom: 1rem !important;
|
||||
}
|
||||
.footer-text{
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Load component based styles
|
||||
@import "_base_component.scss";
|
||||
@import "_registration.scss";
|
||||
@import "_recommendations_page.scss";
|
||||
//
|
||||
// ----------------------------
|
||||
// #COLORS
|
||||
|
||||
Reference in New Issue
Block a user