diff --git a/package-lock.json b/package-lock.json index d8e346e7..b1899a9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3711,9 +3711,9 @@ } }, "@edx/paragon": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-16.6.1.tgz", - "integrity": "sha512-OpfcWhNsgcTfJgxuHm5yZ35FLOPddinENRVd/SzkmUuUYjqsvxlJW5T53yN2OZctP770nvim1qmFAHm+Zyzxhg==", + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-16.10.0.tgz", + "integrity": "sha512-D8QTEf1K/9KNvxguPTpCqErbSAglTCIVsqvHa5T25+345E0o6gexX/FsJVSnT8ZUy3YAXdaPA6FFxhwEXjxPMQ==", "requires": { "@fortawesome/fontawesome-svg-core": "^1.2.30", "@fortawesome/free-solid-svg-icons": "^5.14.0", @@ -8449,6 +8449,16 @@ "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true }, + "clipboard": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.8.tgz", + "integrity": "sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==", + "requires": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -9710,6 +9720,11 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -13363,6 +13378,14 @@ } } }, + "good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "requires": { + "delegate": "^3.1.2" + } + }, "got": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", @@ -22340,6 +22363,11 @@ "commander": "^2.8.1" } }, + "select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -24296,6 +24324,11 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, + "tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, "tiny-invariant": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", diff --git a/package.json b/package.json index 81190d3a..9a5e5adc 100644 --- a/package.json +++ b/package.json @@ -37,13 +37,14 @@ "@edx/brand": "npm:@edx/brand-openedx@1.1.0", "@edx/frontend-component-cookie-policy-banner": "2.1.12", "@edx/frontend-platform": "1.12.4", - "@edx/paragon": "16.6.1", + "@edx/paragon": "16.10.0", "@fortawesome/fontawesome-svg-core": "1.2.32", "@fortawesome/free-brands-svg-icons": "5.15.1", "@fortawesome/free-regular-svg-icons": "5.15.1", "@fortawesome/free-solid-svg-icons": "5.15.1", "@fortawesome/react-fontawesome": "0.1.13", "classnames": "2.2.6", + "clipboard": "^2.0.8", "core-js": "3.9.1", "extract-react-intl-messages": "4.1.1", "fastest-levenshtein": "1.0.12", diff --git a/src/_style.scss b/src/_style.scss index ee991545..2f711e43 100644 --- a/src/_style.scss +++ b/src/_style.scss @@ -351,6 +351,10 @@ select.form-control { height: 282px; } +.variation1-medium-screen { + height: 300px !important; +} + .medium-screen-svg-light, .medium-screen-svg-primary { fill: $light-200; @@ -486,6 +490,14 @@ select.form-control { height: 240px; } +.variation1-bar-color { + stroke: $brand !important; +} + +.variation2-bar-color { + stroke: $accent-a !important; +} + .medium-screen-svg-line { padding-top: 0.5rem; stroke: $accent-b; @@ -670,6 +682,15 @@ select.form-control { font-weight: 400; } +.discount-banner { + background-color: #03C7E8; +} + +.dashed-border { + border-style: dashed; + border-width: thin; + padding: 0.5rem 1rem; +} @media (min-width: 1024px) { .mw-500 { @@ -706,6 +727,12 @@ select.form-control { flex-direction:column; justify-content: center; } + + .dashed-border { + border-style: dashed; + border-width: thin; + padding: 0.25rem 0.5rem; + } } @media (max-width: 1199px) and (min-width: 768px) { @@ -759,6 +786,12 @@ select.form-control { } } +@media (max-width: 550px) { + .variation2-text-alignment { + text-align: left; + } +} + // Smaller than Extra Small (Mobile Screens) @media (max-width: 464px) { .btn-social { diff --git a/src/base-component/BaseComponent.jsx b/src/base-component/BaseComponent.jsx index b7855e14..8093ac4f 100644 --- a/src/base-component/BaseComponent.jsx +++ b/src/base-component/BaseComponent.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import classNames from 'classnames'; import PropTypes from 'prop-types'; @@ -16,12 +16,23 @@ import SmallLayout from './SmallLayout'; import AuthExtraLargeLayout from './AuthExtraLargeLayout'; import AuthMediumLayout from './AuthMediumLayout'; import AuthSmallLayout from './AuthSmallLayout'; +import DiscountExperimentBanner from './DiscountBanner'; -const BaseComponent = ({ children, showWelcomeBanner }) => { +const BaseComponent = ({ children, isRegistrationPage, showWelcomeBanner }) => { const authenticatedUser = showWelcomeBanner ? getAuthenticatedUser() : null; + const [optimizelyExperimentName, setOptimizelyExperimentName] = useState(''); + + useEffect(() => { + const { experimentName } = window; + + if (experimentName) { + setOptimizelyExperimentName(experimentName); + } + }); return ( <> + {isRegistrationPage && optimizelyExperimentName === 'variation2' ? : null}
@@ -33,25 +44,37 @@ const BaseComponent = ({ children, showWelcomeBanner }) => {
- {authenticatedUser ? : } + {authenticatedUser ? : ( + + )}
- {authenticatedUser ? : } + {authenticatedUser ? : ( + + )}
- {authenticatedUser ? : } + {authenticatedUser ? : ( + + )}
- {authenticatedUser ? : } + {authenticatedUser ? : ( + + )} - {authenticatedUser ? : } + {authenticatedUser ? : ( + + )} - {authenticatedUser ? : } + {authenticatedUser ? : ( + + )}
@@ -63,11 +86,13 @@ const BaseComponent = ({ children, showWelcomeBanner }) => { }; BaseComponent.defaultProps = { + isRegistrationPage: false, showWelcomeBanner: false, }; BaseComponent.propTypes = { children: PropTypes.node.isRequired, + isRegistrationPage: PropTypes.bool, showWelcomeBanner: PropTypes.bool, }; diff --git a/src/base-component/DiscountBanner.jsx b/src/base-component/DiscountBanner.jsx new file mode 100644 index 00000000..a5c03fca --- /dev/null +++ b/src/base-component/DiscountBanner.jsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; + +import ClipboardJS from 'clipboard'; + +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { faCut } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { PageBanner } from '@edx/paragon'; + +export default function DiscountExperimentBanner() { + const [show, setShow] = useState(true); + new ClipboardJS('.copyIcon'); // eslint-disable-line no-new + + return ( + <> + { setShow(false); }} + > + + + 15% off , + }} + /> + + + EDXWELCOME + + + + + + ); +} diff --git a/src/base-component/LargeLayout.jsx b/src/base-component/LargeLayout.jsx index 30c292e8..5a5dd69d 100644 --- a/src/base-component/LargeLayout.jsx +++ b/src/base-component/LargeLayout.jsx @@ -1,17 +1,18 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { getConfig } from '@edx/frontend-platform'; import { Hyperlink, Image } from '@edx/paragon'; import LargeScreenLeftLayout from './LargeLeftLayout'; -const LargeLayout = () => ( +const LargeLayout = ({ experimentName, isRegistrationPage }) => (
edx - +
(
); +LargeLayout.defaultProps = { + experimentName: '', + isRegistrationPage: false, +}; + +LargeLayout.propTypes = { + experimentName: PropTypes.string, + isRegistrationPage: PropTypes.bool, +}; + export default LargeLayout; diff --git a/src/base-component/LargeLeftLayout.jsx b/src/base-component/LargeLeftLayout.jsx index 79f4b32c..a8dd7223 100644 --- a/src/base-component/LargeLeftLayout.jsx +++ b/src/base-component/LargeLeftLayout.jsx @@ -1,30 +1,71 @@ import React from 'react'; +import classNames from 'classnames'; + import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; +import PropTypes from 'prop-types'; +import ClipboardJS from 'clipboard'; + +import { faCut } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import messages from './messages'; const LargeLeftLayout = (props) => { - const { intl } = props; - + const { intl, isRegistrationPage, experimentName } = props; + new ClipboardJS('.copyIcon'); // eslint-disable-line no-new return (
- + -

- {intl.formatMessage(messages['start.learning'])} -
- {intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })} -
-

+
+

+ {intl.formatMessage(messages['start.learning'])} + +
+ {intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })} +
+

+ {experimentName === 'variation1' && isRegistrationPage ? ( + + Get 15% off your first verified
certificate* with code
+ + EDXWELCOME + + +
+ ) : null} +
); }; LargeLeftLayout.propTypes = { intl: intlShape.isRequired, + experimentName: PropTypes.string, + isRegistrationPage: PropTypes.bool, +}; + +LargeLeftLayout.defaultProps = { + experimentName: '', + isRegistrationPage: false, }; export default injectIntl(LargeLeftLayout); diff --git a/src/base-component/MediumLayout.jsx b/src/base-component/MediumLayout.jsx index 6c0137e4..10105d3c 100644 --- a/src/base-component/MediumLayout.jsx +++ b/src/base-component/MediumLayout.jsx @@ -1,31 +1,74 @@ import React from 'react'; +import classNames from 'classnames'; + import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; import { Hyperlink, Image } from '@edx/paragon'; +import PropTypes from 'prop-types'; +import ClipboardJS from 'clipboard'; + +import { faCut } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import messages from './messages'; const MediumLayout = (props) => { - const { intl } = props; + const { intl, isRegistrationPage, experimentName } = props; + new ClipboardJS('.copyIcon'); // eslint-disable-line no-new return ( -
+
edx
- + -

- {intl.formatMessage(messages['start.learning'])} -
- {intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })} -
-

+
+

+ {intl.formatMessage(messages['start.learning'])} + +
+ {intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })} +
+

+ {experimentName === 'variation1' && isRegistrationPage ? ( +
+ + Get 15% off your first verified
certificate* with code +
+ + EDXWELCOME + + +
+ ) : null} +
+
@@ -40,6 +83,13 @@ const MediumLayout = (props) => { MediumLayout.propTypes = { intl: intlShape.isRequired, + experimentName: PropTypes.string, + isRegistrationPage: PropTypes.bool, +}; + +MediumLayout.defaultProps = { + experimentName: '', + isRegistrationPage: false, }; export default injectIntl(MediumLayout); diff --git a/src/base-component/SmallLayout.jsx b/src/base-component/SmallLayout.jsx index 9a4379bc..693a18d5 100644 --- a/src/base-component/SmallLayout.jsx +++ b/src/base-component/SmallLayout.jsx @@ -1,14 +1,22 @@ import React from 'react'; +import classNames from 'classnames'; + import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; import { Hyperlink, Image } from '@edx/paragon'; +import PropTypes from 'prop-types'; + +import ClipboardJS from 'clipboard'; + +import { faCut } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import messages from './messages'; const SmallLayout = (props) => { - const { intl } = props; - + const { intl, isRegistrationPage, experimentName } = props; + new ClipboardJS('.copyIcon'); // eslint-disable-line no-new return ( <>
@@ -16,16 +24,41 @@ const SmallLayout = (props) => { edx
- + -

- {intl.formatMessage(messages['start.learning'])} -
- - {intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })} - -

+
+

+ {intl.formatMessage(messages['start.learning'])} +
+ + {intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })} + +

+ {(experimentName === 'variation1' && isRegistrationPage) ? ( +
+ Get 15% off your first verified
certificate* with code
+ + EDXWELCOME + + +
+ ) : null} +
@@ -34,6 +67,14 @@ const SmallLayout = (props) => { SmallLayout.propTypes = { intl: intlShape.isRequired, + experimentName: PropTypes.string, + isRegistrationPage: PropTypes.bool, +}; + +SmallLayout.defaultProps = { + experimentName: '', + isRegistrationPage: false, + }; export default injectIntl(SmallLayout); diff --git a/src/common-components/Logistration.jsx b/src/common-components/Logistration.jsx index bb47abc7..48600c6a 100644 --- a/src/common-components/Logistration.jsx +++ b/src/common-components/Logistration.jsx @@ -52,7 +52,7 @@ const Logistration = (props) => { ); return ( - +
{institutionLogin ? ( diff --git a/src/login/tests/LoginPage.test.jsx b/src/login/tests/LoginPage.test.jsx index 8e2aad1f..28a2affb 100644 --- a/src/login/tests/LoginPage.test.jsx +++ b/src/login/tests/LoginPage.test.jsx @@ -145,7 +145,7 @@ describe('LoginPage', () => { it('should match default button state', () => { const loginPage = mount(reduxWrapper()); - expect(loginPage.find('button[type="submit"] span').first().text()).toEqual('Sign in'); + expect(loginPage.find('button[type="submit"] span').first().text()).toEqual(' Sign in '); }); it('should match pending button state', () => { @@ -160,7 +160,7 @@ describe('LoginPage', () => { const loginPage = mount(reduxWrapper()); const button = loginPage.find('button[type="submit"] span').first(); - expect(button.find('.sr-only').text()).toEqual('pending'); + expect(button.find('.sr-only').text()).toEqual(' pending '); }); it('should show forgot password link', () => { diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index bf833d44..deb7a56f 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -657,6 +657,13 @@ class RegistrationPage extends React.Component { thirdPartyAuthApiStatus, intl)} + {(this.state.optimizelyExperimentName === 'variation1' || this.state.optimizelyExperimentName === 'variation2') + ? ( +
+ {intl.formatMessage(messages['certificate.msg'])} +
+ ) + : null}
); diff --git a/src/register/messages.jsx b/src/register/messages.jsx index e9927a9a..30cc9feb 100644 --- a/src/register/messages.jsx +++ b/src/register/messages.jsx @@ -270,6 +270,11 @@ const messages = defineMessages({ defaultMessage: 'Did you mean', description: 'Did you mean alert suggestion', }, + 'certificate.msg': { + id: 'certificate.msg', + defaultMessage: '*Offer not eligible for GTx’s Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaX’s Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.', + description: 'Text for the 15% discount experiment', + }, }); export default messages; diff --git a/src/register/tests/RegistrationPage.test.jsx b/src/register/tests/RegistrationPage.test.jsx index da71fd18..46f23fc8 100644 --- a/src/register/tests/RegistrationPage.test.jsx +++ b/src/register/tests/RegistrationPage.test.jsx @@ -405,7 +405,7 @@ describe('RegistrationPage', () => { it('should match default button state', () => { const registrationPage = mount(reduxWrapper()); - expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual('Create an account'); + expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual(' Create an account '); }); it('should match pending button state', () => { @@ -420,7 +420,7 @@ describe('RegistrationPage', () => { const registrationPage = mount(reduxWrapper()); const button = registrationPage.find('button[type="submit"] span').first(); - expect(button.find('.sr-only').text()).toEqual('pending'); + expect(button.find('.sr-only').text()).toEqual(' pending '); }); it('should show single sign on provider button', () => {