From 1f21a874b8d9ef04d2b47918e7e249a8a7c14ec1 Mon Sep 17 00:00:00 2001 From: Attiya Ishaque Date: Fri, 17 Feb 2023 17:02:25 +0500 Subject: [PATCH] feat: [VAN-1291] add recommendation page (#743) --- .env | 1 + src/MainApp.jsx | 10 +- .../RedirectLogistration.jsx | 25 ++- src/config/index.js | 1 + src/data/constants.js | 1 + src/data/utils/dataUtils.js | 4 +- .../ProgressiveProfiling.jsx | 36 +++-- src/progressive-profiling/messages.jsx | 5 + src/recommendations/RecommendationCard.jsx | 59 ++++++++ src/recommendations/RecommendationsList.jsx | 50 ++++++ src/recommendations/RecommendationsPage.jsx | 143 ++++++++++++++++++ src/recommendations/index.js | 1 + src/recommendations/messages.js | 21 +++ src/sass/_recommendations_page.scss | 66 ++++++++ src/sass/_style.scss | 1 + 15 files changed, 409 insertions(+), 15 deletions(-) create mode 100644 src/recommendations/RecommendationCard.jsx create mode 100644 src/recommendations/RecommendationsList.jsx create mode 100644 src/recommendations/RecommendationsPage.jsx create mode 100644 src/recommendations/index.js create mode 100644 src/recommendations/messages.js create mode 100644 src/sass/_recommendations_page.scss diff --git a/.env b/.env index fc3fc60a..0556efc7 100644 --- a/.env +++ b/.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 ***** diff --git a/src/MainApp.jsx b/src/MainApp.jsx index 658440fd..274e143a 100755 --- a/src/MainApp.jsx +++ b/src/MainApp.jsx @@ -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 = () => ( + diff --git a/src/common-components/RedirectLogistration.jsx b/src/common-components/RedirectLogistration.jsx index 6c332603..00dec515 100644 --- a/src/common-components/RedirectLogistration.jsx +++ b/src/common-components/RedirectLogistration.jsx @@ -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 ( + + ); + } + 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; diff --git a/src/config/index.js b/src/config/index.js index c7df1ba2..c5e0dae5 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -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, diff --git a/src/data/constants.js b/src/data/constants.js index 656acb4f..eb1a1962 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -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'; diff --git a/src/data/utils/dataUtils.js b/src/data/utils/dataUtils.js index 2442fe5e..61669c8b 100644 --- a/src/data/utils/dataUtils.js +++ b/src/data/utils/dataUtils.js @@ -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) { diff --git a/src/progressive-profiling/ProgressiveProfiling.jsx b/src/progressive-profiling/ProgressiveProfiling.jsx index cc21f33b..968c6d3e 100644 --- a/src/progressive-profiling/ProgressiveProfiling.jsx +++ b/src/progressive-profiling/ProgressiveProfiling.jsx @@ -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) => { ) : null}
@@ -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} diff --git a/src/progressive-profiling/messages.jsx b/src/progressive-profiling/messages.jsx index 1a15a014..bdf31c64 100644 --- a/src/progressive-profiling/messages.jsx +++ b/src/progressive-profiling/messages.jsx @@ -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', diff --git a/src/recommendations/RecommendationCard.jsx b/src/recommendations/RecommendationCard.jsx new file mode 100644 index 00000000..77fa3d54 --- /dev/null +++ b/src/recommendations/RecommendationCard.jsx @@ -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 ( +
+ + + + + + Course} /> + + +
+ ); +}; + +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); diff --git a/src/recommendations/RecommendationsList.jsx b/src/recommendations/RecommendationsList.jsx new file mode 100644 index 00000000..53570554 --- /dev/null +++ b/src/recommendations/RecommendationsList.jsx @@ -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 ( + +

+ {title} +

+
+ { + recommendations.map((recommendation) => ( + + )) + } +
+
+ ); +}; + +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); diff --git a/src/recommendations/RecommendationsPage.jsx b/src/recommendations/RecommendationsPage.jsx new file mode 100644 index 00000000..5f6a95e7 --- /dev/null +++ b/src/recommendations/RecommendationsPage.jsx @@ -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 ( +
+
+
+ + {getConfig().SITE_NAME} + +
+
+ +
+ +
+
+
+ ); +}; + +RecommendationsPage.propTypes = { + intl: PropTypes.objectOf(PropTypes.object).isRequired, + location: PropTypes.shape({ + state: PropTypes.object, + }), + +}; + +RecommendationsPage.defaultProps = { + location: { state: {} }, +}; + +export default injectIntl(RecommendationsPage); diff --git a/src/recommendations/index.js b/src/recommendations/index.js new file mode 100644 index 00000000..1c17a099 --- /dev/null +++ b/src/recommendations/index.js @@ -0,0 +1 @@ +export { default } from './RecommendationsPage'; diff --git a/src/recommendations/messages.js b/src/recommendations/messages.js new file mode 100644 index 00000000..3922a264 --- /dev/null +++ b/src/recommendations/messages.js @@ -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; diff --git a/src/sass/_recommendations_page.scss b/src/sass/_recommendations_page.scss new file mode 100644 index 00000000..f37dbf03 --- /dev/null +++ b/src/sass/_recommendations_page.scss @@ -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; + } +} diff --git a/src/sass/_style.scss b/src/sass/_style.scss index ce7ac9f9..5972e055 100644 --- a/src/sass/_style.scss +++ b/src/sass/_style.scss @@ -1,6 +1,7 @@ // Load component based styles @import "_base_component.scss"; @import "_registration.scss"; +@import "_recommendations_page.scss"; // // ---------------------------- // #COLORS