feat: [VAN-1291] add recommendation page (#743)

This commit is contained in:
Attiya Ishaque
2023-02-17 17:02:25 +05:00
committed by GitHub
parent ee6a6f0d2d
commit 1f21a874b8
15 changed files with 409 additions and 15 deletions

1
.env
View File

@@ -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 *****

View File

@@ -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} />

View File

@@ -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;

View File

@@ -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,

View File

@@ -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';

View File

@@ -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) {

View File

@@ -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}

View File

@@ -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',

View 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);

View 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);

View 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);

View File

@@ -0,0 +1 @@
export { default } from './RecommendationsPage';

View 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;

View 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;
}
}

View File

@@ -1,6 +1,7 @@
// Load component based styles
@import "_base_component.scss";
@import "_registration.scss";
@import "_recommendations_page.scss";
//
// ----------------------------
// #COLORS