mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-29 15:12:58 +03:00
Refactored code structure using react context
ref https://github.com/TryGhost/members.js/issues/5 React context allows us cleaner setup in codebase with shared data and methods across components at different nesting levels. This should allow faster iteration and easier development going forward. - Uses Parent context for shared data and methods across different components instead of passed down props - Uses new `init` method in API for data initialization - Removes `PopupMenu` component in favor of `PopupModal` - Adds new test util for custom render for easier base setup - https://testing-library.com/docs/react-testing-library/setup#custom-render - Updates tests to use new test util for easier test setup
This commit is contained in:
parent
2214a5048b
commit
487bea51b2
@ -1,11 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {render} from '@testing-library/react';
|
import {render} from '@testing-library/react';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import {site} from './test/fixtures/data';
|
|
||||||
|
|
||||||
test('renders App', () => {
|
test('renders App', () => {
|
||||||
const {container} = render(
|
const {container} = render(
|
||||||
<App data={{site}} />
|
<App data={{adminUrl: 'https://youradminurl.com'}} />
|
||||||
);
|
);
|
||||||
|
|
||||||
// dashboard component should be rendered on root route
|
// dashboard component should be rendered on root route
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import TriggerButton from './TriggerButton';
|
import TriggerButton from './TriggerButton';
|
||||||
import PopupMenu from './PopupMenu';
|
|
||||||
import PopupModal from './PopupModal';
|
import PopupModal from './PopupModal';
|
||||||
import * as Fixtures from '../test/fixtures/data';
|
import * as Fixtures from '../test/fixtures/data';
|
||||||
import Api from '../utils/api';
|
import setupGhostApi from '../utils/api';
|
||||||
|
import {ParentContext} from './ParentContext';
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
|
|
||||||
export default class ParentContainer extends React.Component {
|
export default class ParentContainer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
data: PropTypes.object.isRequired
|
data: PropTypes.object.isRequired
|
||||||
@ -14,57 +15,64 @@ export default class ParentContainer extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
// Setup custom trigger button handling
|
||||||
page: 'magiclink',
|
this.setupCustomTriggerButton();
|
||||||
showPopup: false,
|
|
||||||
action: {
|
|
||||||
name: 'loading'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.initialize();
|
this.state = {
|
||||||
|
page: 'accountHome',
|
||||||
|
showPopup: false,
|
||||||
|
action: 'init:running',
|
||||||
|
initStatus: 'running'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Initialize site and members data
|
|
||||||
|
|
||||||
this.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
// Setup custom trigger button handling
|
|
||||||
this.setupCustomTriggerButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadData() {
|
|
||||||
// Setup Members API with site/admin URLs
|
|
||||||
const {adminUrl} = this.props.data;
|
const {adminUrl} = this.props.data;
|
||||||
const siteUrl = window.location.origin;
|
if (adminUrl) {
|
||||||
this.MembersAPI = Api({siteUrl, adminUrl});
|
this.GhostApi = setupGhostApi({adminUrl});
|
||||||
|
this.fetchData();
|
||||||
|
} else {
|
||||||
|
console.error(`[Members.js] Failed to initialize, pass a valid admin url.`);
|
||||||
|
this.setState({
|
||||||
|
action: 'init:failed:missingAdminUrl'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch site and member session data with Ghost Apis
|
||||||
|
async fetchData() {
|
||||||
|
const {adminUrl} = this.props.data;
|
||||||
|
this.GhostApi = setupGhostApi({adminUrl});
|
||||||
try {
|
try {
|
||||||
const [{site}, member] = await Promise.all([this.MembersAPI.site.read(), this.MembersAPI.member.sessionData()]);
|
const {site, member} = await this.GhostApi.init();
|
||||||
console.log('Initialized Members.js with', site, member);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
site,
|
site,
|
||||||
member,
|
member,
|
||||||
page: member ? 'accountHome' : 'signup',
|
page: member ? 'accountHome' : 'signup',
|
||||||
action: 'init:success'
|
action: 'init:success',
|
||||||
|
initStatus: 'success'
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Failed state fetch', e);
|
console.error(`[Members.js] Failed to fetch site data, please make sure your admin url - ${adminUrl} - is correct.`);
|
||||||
this.setState({
|
this.setState({
|
||||||
action: {
|
action: 'init:failed:incorrectAdminUrl',
|
||||||
name: 'init:failed'
|
initStatus: 'failed'
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getData() {
|
getData() {
|
||||||
const member = process.env.REACT_APP_ADMIN_URL ? Fixtures.member.free : this.state.member;
|
// Load data from fixtures for development mode
|
||||||
const site = process.env.REACT_APP_ADMIN_URL ? Fixtures.site : this.state.site;
|
if (process.env.REACT_APP_ADMIN_URL) {
|
||||||
|
return {
|
||||||
return {site, member};
|
site: Fixtures.site,
|
||||||
|
member: Fixtures.member.free
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
site: this.state.site,
|
||||||
|
member: this.state.member
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
switchPage(page) {
|
switchPage(page) {
|
||||||
@ -74,7 +82,8 @@ export default class ParentContainer extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupCustomTriggerButton() {
|
setupCustomTriggerButton() {
|
||||||
this.customTriggerButton = document.querySelector('[data-members-trigger-button]');
|
const customTriggerSelector = '[data-members-trigger-button]';
|
||||||
|
this.customTriggerButton = document.querySelector(customTriggerSelector);
|
||||||
|
|
||||||
if (this.customTriggerButton) {
|
if (this.customTriggerButton) {
|
||||||
const clickHandler = (event) => {
|
const clickHandler = (event) => {
|
||||||
@ -83,78 +92,56 @@ export default class ParentContainer extends React.Component {
|
|||||||
const elRemoveClass = this.state.showPopup ? 'popup-open' : 'popup-close';
|
const elRemoveClass = this.state.showPopup ? 'popup-open' : 'popup-close';
|
||||||
this.customTriggerButton.classList.add(elAddClass);
|
this.customTriggerButton.classList.add(elAddClass);
|
||||||
this.customTriggerButton.classList.remove(elRemoveClass);
|
this.customTriggerButton.classList.remove(elRemoveClass);
|
||||||
this.onTriggerToggle();
|
this.onAction('togglePopup');
|
||||||
};
|
};
|
||||||
this.customTriggerButton.classList.add('popup-close');
|
this.customTriggerButton.classList.add('popup-close');
|
||||||
this.customTriggerButton.addEventListener('click', clickHandler);
|
this.customTriggerButton.addEventListener('click', clickHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetAction() {
|
|
||||||
this.setState({
|
|
||||||
action: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getBrandColor() {
|
getBrandColor() {
|
||||||
return this.getData().site && this.getData().site.brand && this.getData().site.brand.primaryColor;
|
return (this.getData().site && this.getData().site.brand && this.getData().site.brand.primaryColor) || '#3db0ef';
|
||||||
}
|
}
|
||||||
|
|
||||||
async onAction(action, data) {
|
async onAction(action, data) {
|
||||||
this.setState({
|
this.setState({
|
||||||
action: {
|
action: `${action}:running`
|
||||||
name: action,
|
|
||||||
isRunning: true,
|
|
||||||
isSuccess: false,
|
|
||||||
error: null
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
if (action === 'closePopup') {
|
if (action === 'switchPage') {
|
||||||
|
this.setState({
|
||||||
|
page: data
|
||||||
|
});
|
||||||
|
} else if (action === 'togglePopup') {
|
||||||
|
this.setState({
|
||||||
|
showPopup: !this.state.showPopup
|
||||||
|
});
|
||||||
|
} else if (action === 'closePopup') {
|
||||||
this.setState({
|
this.setState({
|
||||||
showPopup: false
|
showPopup: false
|
||||||
});
|
});
|
||||||
} else if (action === 'signout') {
|
} else if (action === 'signout') {
|
||||||
await this.MembersAPI.member.signout();
|
await this.GhostApi.member.signout();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
action: {
|
action: 'signout:success'
|
||||||
name: action,
|
|
||||||
isRunning: false,
|
|
||||||
isSuccess: true
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
} else if (action === 'signin') {
|
||||||
|
await this.GhostApi.member.sendMagicLink(data);
|
||||||
if (action === 'signin') {
|
|
||||||
await this.MembersAPI.member.sendMagicLink(data);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
action: {
|
action: 'signin:success',
|
||||||
name: action,
|
|
||||||
isRunning: false,
|
|
||||||
isSuccess: true
|
|
||||||
},
|
|
||||||
page: 'magiclink'
|
page: 'magiclink'
|
||||||
});
|
});
|
||||||
}
|
} else if (action === 'signup') {
|
||||||
|
await this.GhostApi.member.sendMagicLink(data);
|
||||||
if (action === 'signup') {
|
|
||||||
await this.MembersAPI.member.sendMagicLink(data);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
action: {
|
action: 'signup:success',
|
||||||
name: action,
|
|
||||||
isRunning: false,
|
|
||||||
isSuccess: true
|
|
||||||
},
|
|
||||||
page: 'magiclink'
|
page: 'magiclink'
|
||||||
});
|
});
|
||||||
}
|
} else if (action === 'checkoutPlan') {
|
||||||
|
|
||||||
if (action === 'checkoutPlan') {
|
|
||||||
const checkoutSuccessUrl = (new URL('/account/?stripe=billing-update-success', window.location.href)).href;
|
const checkoutSuccessUrl = (new URL('/account/?stripe=billing-update-success', window.location.href)).href;
|
||||||
const checkoutCancelUrl = (new URL('/account/?stripe=billing-update-cancel', window.location.href)).href;
|
const checkoutCancelUrl = (new URL('/account/?stripe=billing-update-cancel', window.location.href)).href;
|
||||||
const {plan} = data;
|
const {plan} = data;
|
||||||
await this.MembersAPI.member.checkoutPlan({
|
await this.GhostApi.member.checkoutPlan({
|
||||||
plan,
|
plan,
|
||||||
checkoutSuccessUrl,
|
checkoutSuccessUrl,
|
||||||
checkoutCancelUrl
|
checkoutCancelUrl
|
||||||
@ -162,47 +149,15 @@ export default class ParentContainer extends React.Component {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({
|
this.setState({
|
||||||
action: {
|
action: `${action}:failed`
|
||||||
name: action,
|
|
||||||
isRunning: false,
|
|
||||||
error: e
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTriggerToggle() {
|
|
||||||
let showPopup = !this.state.showPopup;
|
|
||||||
this.setState({
|
|
||||||
showPopup
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPopupMenu() {
|
renderPopupMenu() {
|
||||||
if (this.state.showPopup) {
|
if (this.state.showPopup) {
|
||||||
if (this.state.page === 'accountHome') {
|
|
||||||
return (
|
|
||||||
<PopupMenu
|
|
||||||
data={this.getData()}
|
|
||||||
action={this.state.action}
|
|
||||||
onToggle= {e => this.onTriggerToggle()}
|
|
||||||
page={this.state.page}
|
|
||||||
switchPage={page => this.switchPage(page)}
|
|
||||||
onAction={(action, data) => this.onAction(action, data)}
|
|
||||||
brandColor = {this.getBrandColor()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<PopupModal
|
<PopupModal />
|
||||||
data={this.getData()}
|
|
||||||
action={this.state.action}
|
|
||||||
onToggle= {e => this.onTriggerToggle()}
|
|
||||||
page={this.state.page}
|
|
||||||
switchPage={page => this.switchPage(page)}
|
|
||||||
onAction={(action, data) => this.onAction(action, data)}
|
|
||||||
brandColor = {this.getBrandColor()}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -212,11 +167,7 @@ export default class ParentContainer extends React.Component {
|
|||||||
if (!this.customTriggerButton) {
|
if (!this.customTriggerButton) {
|
||||||
return (
|
return (
|
||||||
<TriggerButton
|
<TriggerButton
|
||||||
name={this.props.name}
|
|
||||||
onToggle= {e => this.onTriggerToggle()}
|
|
||||||
isPopupOpen={this.state.showPopup}
|
isPopupOpen={this.state.showPopup}
|
||||||
data={this.getData()}
|
|
||||||
brandColor = {this.getBrandColor()}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -224,12 +175,29 @@ export default class ParentContainer extends React.Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getActionData(action) {
|
||||||
|
const [type, status, reason] = action.split(':');
|
||||||
|
return {type, status, reason};
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
if (this.state.initStatus === 'success' || process.env.REACT_APP_ADMIN_URL) {
|
||||||
<>
|
const {site, member} = this.getData();
|
||||||
{this.renderPopupMenu()}
|
|
||||||
{this.renderTriggerButton()}
|
return (
|
||||||
</>
|
<ParentContext.Provider value={{
|
||||||
);
|
site,
|
||||||
|
member,
|
||||||
|
action: this.state.action,
|
||||||
|
brandColor: this.getBrandColor(),
|
||||||
|
page: this.state.page,
|
||||||
|
onAction: (action, data) => this.onAction(action, data)
|
||||||
|
}}>
|
||||||
|
{this.renderPopupMenu()}
|
||||||
|
{this.renderTriggerButton()}
|
||||||
|
</ParentContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,144 +0,0 @@
|
|||||||
import Frame from './Frame';
|
|
||||||
import SigninPage from './pages/SigninPage';
|
|
||||||
import SignupPage from './pages/SignupPage';
|
|
||||||
import AccountHomePage from './pages/AccountHomePage';
|
|
||||||
import MagicLinkPage from './pages/MagicLinkPage';
|
|
||||||
import LoadingPage from './pages/LoadingPage';
|
|
||||||
|
|
||||||
const React = require('react');
|
|
||||||
const PropTypes = require('prop-types');
|
|
||||||
|
|
||||||
const Styles = {
|
|
||||||
frame: {
|
|
||||||
common: {
|
|
||||||
zIndex: '2147483000',
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: '100px',
|
|
||||||
right: '20px',
|
|
||||||
width: '350px',
|
|
||||||
minHeight: '350px',
|
|
||||||
maxHeight: '410px',
|
|
||||||
boxShadow: 'rgba(0, 0, 0, 0.16) 0px 5px 40px',
|
|
||||||
opacity: '1',
|
|
||||||
height: 'calc(100% - 120px)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
backgroundColor: 'white'
|
|
||||||
},
|
|
||||||
signin: {
|
|
||||||
width: '400px',
|
|
||||||
minHeight: '200px',
|
|
||||||
maxHeight: '240px'
|
|
||||||
},
|
|
||||||
signup: {
|
|
||||||
width: '450px',
|
|
||||||
minHeight: '400px',
|
|
||||||
maxHeight: '460px'
|
|
||||||
},
|
|
||||||
accountHome: {
|
|
||||||
width: '280px',
|
|
||||||
minHeight: '200px',
|
|
||||||
maxHeight: '240px'
|
|
||||||
},
|
|
||||||
magiclink: {
|
|
||||||
width: '400px',
|
|
||||||
minHeight: '130px',
|
|
||||||
maxHeight: '130px'
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
width: '250px',
|
|
||||||
minHeight: '130px',
|
|
||||||
maxHeight: '130px'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
popup: {
|
|
||||||
parent: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
position: 'absolute',
|
|
||||||
letterSpacing: '0',
|
|
||||||
textRendering: 'optimizeLegibility',
|
|
||||||
fontSize: '1.5rem'
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
position: 'absolute',
|
|
||||||
top: '0px',
|
|
||||||
bottom: '0px',
|
|
||||||
left: '0px',
|
|
||||||
right: '0px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
paddingTop: '18px',
|
|
||||||
paddingBottom: '18px',
|
|
||||||
textAlign: 'left'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Pages = {
|
|
||||||
signin: SigninPage,
|
|
||||||
signup: SignupPage,
|
|
||||||
accountHome: AccountHomePage,
|
|
||||||
magiclink: MagicLinkPage,
|
|
||||||
loading: LoadingPage
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class PopupMenu extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
data: PropTypes.shape({
|
|
||||||
site: PropTypes.shape({
|
|
||||||
title: PropTypes.string,
|
|
||||||
description: PropTypes.string
|
|
||||||
}).isRequired,
|
|
||||||
member: PropTypes.shape({
|
|
||||||
email: PropTypes.string
|
|
||||||
})
|
|
||||||
}).isRequired,
|
|
||||||
action: PropTypes.object,
|
|
||||||
page: PropTypes.string.isRequired,
|
|
||||||
onAction: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
renderCurrentPage(page) {
|
|
||||||
const PageComponent = Pages[page];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageComponent
|
|
||||||
data={this.props.data}
|
|
||||||
action={this.props.action}
|
|
||||||
onAction={(action, data) => this.props.onAction(action, data)}
|
|
||||||
switchPage={page => this.props.switchPage(page)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPopupContent() {
|
|
||||||
return (
|
|
||||||
<div style={Styles.popup.parent}>
|
|
||||||
<div style={Styles.popup.container}>
|
|
||||||
{this.renderCurrentPage(this.props.page)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFrameContainer() {
|
|
||||||
const page = this.props.page;
|
|
||||||
const frameStyle = {
|
|
||||||
...Styles.frame.common,
|
|
||||||
...Styles.frame[page]
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Frame style={frameStyle} title="membersjs-popup">
|
|
||||||
{this.renderPopupContent()}
|
|
||||||
</Frame>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return this.renderFrameContainer();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {render} from '@testing-library/react';
|
|
||||||
import PopupMenu from './PopupMenu';
|
|
||||||
import {site} from '../test/fixtures/data';
|
|
||||||
|
|
||||||
describe('Popup Menu', () => {
|
|
||||||
test('renders', () => {
|
|
||||||
const {getByTitle} = render(
|
|
||||||
<PopupMenu data={{site}} page='signin' action={{}} onAction={() => {}} />
|
|
||||||
);
|
|
||||||
const popupFrame = getByTitle('membersjs-popup');
|
|
||||||
|
|
||||||
expect(popupFrame).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
@ -5,9 +5,9 @@ import AccountHomePage from './pages/AccountHomePage';
|
|||||||
import MagicLinkPage from './pages/MagicLinkPage';
|
import MagicLinkPage from './pages/MagicLinkPage';
|
||||||
import LoadingPage from './pages/LoadingPage';
|
import LoadingPage from './pages/LoadingPage';
|
||||||
import {ReactComponent as CloseIcon} from '../images/icons/close.svg';
|
import {ReactComponent as CloseIcon} from '../images/icons/close.svg';
|
||||||
|
import {ParentContext} from './ParentContext';
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
|
||||||
|
|
||||||
const Styles = {
|
const Styles = {
|
||||||
modalContainer: {
|
modalContainer: {
|
||||||
@ -35,6 +35,19 @@ const Styles = {
|
|||||||
height: '60%',
|
height: '60%',
|
||||||
backgroundColor: 'white'
|
backgroundColor: 'white'
|
||||||
},
|
},
|
||||||
|
menu: {
|
||||||
|
position: 'fixed',
|
||||||
|
padding: '0',
|
||||||
|
outline: '0',
|
||||||
|
bottom: '100px',
|
||||||
|
right: '20px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: 'rgba(0, 0, 0, 0.16) 0px 5px 40px',
|
||||||
|
opacity: '1',
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: '60%',
|
||||||
|
backgroundColor: 'white'
|
||||||
|
},
|
||||||
signin: {
|
signin: {
|
||||||
minHeight: '200px',
|
minHeight: '200px',
|
||||||
maxHeight: '330px'
|
maxHeight: '330px'
|
||||||
@ -44,8 +57,9 @@ const Styles = {
|
|||||||
maxHeight: '620px'
|
maxHeight: '620px'
|
||||||
},
|
},
|
||||||
accountHome: {
|
accountHome: {
|
||||||
minHeight: '350px',
|
width: '280px',
|
||||||
maxHeight: '510px'
|
minHeight: '200px',
|
||||||
|
maxHeight: '240px'
|
||||||
},
|
},
|
||||||
magiclink: {
|
magiclink: {
|
||||||
minHeight: '230px',
|
minHeight: '230px',
|
||||||
@ -94,39 +108,20 @@ const Pages = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default class PopupModal extends React.Component {
|
export default class PopupModal extends React.Component {
|
||||||
static propTypes = {
|
static contextType = ParentContext;
|
||||||
data: PropTypes.shape({
|
|
||||||
site: PropTypes.shape({
|
|
||||||
title: PropTypes.string,
|
|
||||||
description: PropTypes.string
|
|
||||||
}).isRequired,
|
|
||||||
member: PropTypes.shape({
|
|
||||||
email: PropTypes.string
|
|
||||||
})
|
|
||||||
}).isRequired,
|
|
||||||
action: PropTypes.object,
|
|
||||||
page: PropTypes.string.isRequired,
|
|
||||||
onAction: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
renderCurrentPage(page) {
|
renderCurrentPage(page) {
|
||||||
const PageComponent = Pages[page];
|
const PageComponent = Pages[page];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageComponent
|
<PageComponent />
|
||||||
data={this.props.data}
|
|
||||||
action={this.props.action}
|
|
||||||
onAction={(action, data) => this.props.onAction(action, data)}
|
|
||||||
brandColor={this.props.brandColor}
|
|
||||||
switchPage={page => this.props.switchPage(page)}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPopupClose() {
|
renderPopupClose() {
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', justifyContent: 'flex-end', padding: '0 20px'}}>
|
<div style={{display: 'flex', justifyContent: 'flex-end', padding: '0 20px'}}>
|
||||||
<CloseIcon style={Styles.popup.closeIcon} onClick = {() => this.props.onToggle()} />
|
<CloseIcon style={Styles.popup.closeIcon} onClick = {() => this.context.onAction('closePopup')} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -135,7 +130,7 @@ export default class PopupModal extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div style={Styles.popup.container}>
|
<div style={Styles.popup.container}>
|
||||||
{this.renderPopupClose()}
|
{this.renderPopupClose()}
|
||||||
{this.renderCurrentPage(this.props.page)}
|
{this.renderCurrentPage(this.context.page)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -143,14 +138,15 @@ export default class PopupModal extends React.Component {
|
|||||||
handlePopupClose(e) {
|
handlePopupClose(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
this.props.onToggle();
|
this.context.onAction('closePopup');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFrameContainer() {
|
renderFrameContainer() {
|
||||||
const page = this.props.page;
|
const page = this.context.page;
|
||||||
|
const commonStyle = this.context.page === 'accountHome' ? Styles.frame.menu : Styles.frame.common;
|
||||||
const frameStyle = {
|
const frameStyle = {
|
||||||
...Styles.frame.common,
|
...commonStyle,
|
||||||
...Styles.frame[page]
|
...Styles.frame[page]
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
@ -1,94 +1,93 @@
|
|||||||
import Frame from './Frame';
|
import Frame from './Frame';
|
||||||
|
import {ParentContext} from './ParentContext';
|
||||||
import {ReactComponent as UserIcon} from '../images/icons/user.svg';
|
import {ReactComponent as UserIcon} from '../images/icons/user.svg';
|
||||||
import {ReactComponent as CloseIcon} from '../images/icons/close.svg';
|
import {ReactComponent as CloseIcon} from '../images/icons/close.svg';
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
|
||||||
|
|
||||||
const Styles = {
|
const Styles = ({brandColor}) => {
|
||||||
frame: {
|
return {
|
||||||
zIndex: '2147483000',
|
frame: {
|
||||||
position: 'fixed',
|
zIndex: '2147483000',
|
||||||
bottom: '20px',
|
position: 'fixed',
|
||||||
right: '20px',
|
bottom: '20px',
|
||||||
width: '60px',
|
right: '20px',
|
||||||
height: '60px',
|
width: '60px',
|
||||||
boxShadow: 'rgba(0, 0, 0, 0.06) 0px 1px 6px 0px, rgba(0, 0, 0, 0.16) 0px 2px 32px 0px',
|
height: '60px',
|
||||||
borderRadius: '50%',
|
boxShadow: 'rgba(0, 0, 0, 0.06) 0px 1px 6px 0px, rgba(0, 0, 0, 0.16) 0px 2px 32px 0px',
|
||||||
backgroundColor: '#3EB0EF',
|
borderRadius: '50%',
|
||||||
animation: '250ms ease 0s 1 normal none running animation-bhegco',
|
backgroundColor: brandColor,
|
||||||
transition: 'opacity 0.3s ease 0s'
|
animation: '250ms ease 0s 1 normal none running animation-bhegco',
|
||||||
},
|
transition: 'opacity 0.3s ease 0s'
|
||||||
launcher: {
|
},
|
||||||
position: 'absolute',
|
launcher: {
|
||||||
top: '0px',
|
position: 'absolute',
|
||||||
left: '0px',
|
top: '0px',
|
||||||
width: '60px',
|
left: '0px',
|
||||||
height: '60px',
|
width: '60px',
|
||||||
cursor: 'pointer',
|
height: '60px',
|
||||||
transformOrigin: 'center center',
|
cursor: 'pointer',
|
||||||
backfaceVisibility: 'hidden',
|
transformOrigin: 'center center',
|
||||||
WebkitFontSmoothing: 'antialiased',
|
backfaceVisibility: 'hidden',
|
||||||
borderRadius: '50%',
|
WebkitFontSmoothing: 'antialiased',
|
||||||
overflow: 'hidden'
|
borderRadius: '50%',
|
||||||
},
|
overflow: 'hidden'
|
||||||
button: {
|
},
|
||||||
display: 'flex',
|
button: {
|
||||||
WebkitBoxAlign: 'center',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
WebkitBoxAlign: 'center',
|
||||||
WebkitBoxPack: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
WebkitBoxPack: 'center',
|
||||||
position: 'absolute',
|
justifyContent: 'center',
|
||||||
top: '0px',
|
position: 'absolute',
|
||||||
bottom: '0px',
|
top: '0px',
|
||||||
width: '100%',
|
bottom: '0px',
|
||||||
opacity: '1',
|
width: '100%',
|
||||||
transform: 'rotate(0deg) scale(1)',
|
opacity: '1',
|
||||||
transition: 'transform 0.16s linear 0s, opacity 0.08s linear 0s'
|
transform: 'rotate(0deg) scale(1)',
|
||||||
},
|
transition: 'transform 0.16s linear 0s, opacity 0.08s linear 0s'
|
||||||
userIcon: {
|
},
|
||||||
width: '20px',
|
userIcon: {
|
||||||
height: '20px',
|
width: '20px',
|
||||||
color: '#fff'
|
height: '20px',
|
||||||
},
|
color: '#fff'
|
||||||
|
},
|
||||||
|
|
||||||
closeIcon: {
|
closeIcon: {
|
||||||
width: '20px',
|
width: '20px',
|
||||||
height: '20px',
|
height: '20px',
|
||||||
color: '#fff'
|
color: '#fff'
|
||||||
}
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class TriggerButton extends React.Component {
|
export default class TriggerButton extends React.Component {
|
||||||
static propTypes = {
|
static contextType = ParentContext;
|
||||||
name: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
onToggle() {
|
onToggle() {
|
||||||
this.props.onToggle();
|
this.context.onAction('togglePopup');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTriggerIcon() {
|
renderTriggerIcon() {
|
||||||
|
const Style = Styles({brandColor: this.context.brandColor});
|
||||||
|
|
||||||
if (this.props.isPopupOpen) {
|
if (this.props.isPopupOpen) {
|
||||||
return (
|
return (
|
||||||
<CloseIcon style={Styles.closeIcon} />
|
<CloseIcon style={Style.closeIcon} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserIcon style={Styles.userIcon} />
|
<UserIcon style={Style.userIcon} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const frameStyle = {
|
const Style = Styles({brandColor: this.context.brandColor});
|
||||||
...Styles.frame,
|
|
||||||
backgroundColor: this.props.brandColor || '#3EB0EF'
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Frame style={frameStyle} title="membersjs-trigger">
|
<Frame style={Style.frame} title="membersjs-trigger">
|
||||||
<div style={Styles.launcher} onClick={e => this.onToggle(e)}>
|
<div style={Style.launcher} onClick={e => this.onToggle(e)}>
|
||||||
<div style={Styles.button}>
|
<div style={Style.button}>
|
||||||
{this.renderTriggerIcon()}
|
{this.renderTriggerIcon()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,34 +1,20 @@
|
|||||||
|
import {ParentContext} from '../ParentContext';
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
|
||||||
|
|
||||||
export default class AccountHomePage extends React.Component {
|
export default class AccountHomePage extends React.Component {
|
||||||
static propTypes = {
|
static contextType = ParentContext;
|
||||||
data: PropTypes.shape({
|
|
||||||
site: PropTypes.shape({
|
|
||||||
title: PropTypes.string,
|
|
||||||
description: PropTypes.string
|
|
||||||
}).isRequired,
|
|
||||||
member: PropTypes.shape({
|
|
||||||
email: PropTypes.string
|
|
||||||
}).isRequired
|
|
||||||
}).isRequired,
|
|
||||||
onAction: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSignout(e) {
|
handleSignout(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onAction('signout');
|
this.context.onAction('signout');
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePlanCheckout(e) {
|
handlePlanCheckout(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const plan = e.target.name;
|
const plan = e.target.name;
|
||||||
const email = this.getMemberEmail();
|
const email = this.context.member.email;
|
||||||
this.props.onAction('checkoutPlan', {email, plan});
|
this.context.onAction('checkoutPlan', {email, plan});
|
||||||
}
|
|
||||||
|
|
||||||
getMemberEmail() {
|
|
||||||
return this.props.data.member.email;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPlanSelectButton({name}) {
|
renderPlanSelectButton({name}) {
|
||||||
@ -45,7 +31,7 @@ export default class AccountHomePage extends React.Component {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: '.4s ease',
|
transition: '.4s ease',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
backgroundColor: this.props.brandColor || '#3eb0ef',
|
backgroundColor: this.context.brandColor,
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
width: '90px',
|
width: '90px',
|
||||||
@ -110,8 +96,8 @@ export default class AccountHomePage extends React.Component {
|
|||||||
border: '1px solid black',
|
border: '1px solid black',
|
||||||
marginBottom: '12px'
|
marginBottom: '12px'
|
||||||
};
|
};
|
||||||
const siteTitle = this.props.data.site && this.props.data.site.title;
|
const siteTitle = this.context.site.title;
|
||||||
const plans = this.props.data.site && this.props.data.site.plans;
|
const plans = this.context.site.plans;
|
||||||
return (
|
return (
|
||||||
<div style={{padding: '12px 12px'}}>
|
<div style={{padding: '12px 12px'}}>
|
||||||
<div style={{marginBottom: '12px', fontSize: '14px'}}>
|
<div style={{marginBottom: '12px', fontSize: '14px'}}>
|
||||||
@ -127,7 +113,7 @@ export default class AccountHomePage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderHeader() {
|
renderHeader() {
|
||||||
const memberEmail = this.getMemberEmail();
|
const memberEmail = this.context.member.email;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -142,7 +128,7 @@ export default class AccountHomePage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderUserAvatar() {
|
renderUserAvatar() {
|
||||||
const avatarImg = (this.props.data.member && this.props.data.member.avatar_image);
|
const avatarImg = (this.context.member && this.context.member.avatar_image);
|
||||||
|
|
||||||
const logoStyle = {
|
const logoStyle = {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@ -167,8 +153,8 @@ export default class AccountHomePage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderUserHeader() {
|
renderUserHeader() {
|
||||||
const memberEmail = this.getMemberEmail();
|
const memberEmail = this.context.member.email;
|
||||||
const memberName = this.props.data.member.name;
|
const memberName = this.context.member.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '24px'}}>
|
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '24px'}}>
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {render, fireEvent} from '@testing-library/react';
|
import {render, fireEvent} from '../../utils/tests';
|
||||||
import AccountHomePage from './AccountHomePage';
|
import AccountHomePage from './AccountHomePage';
|
||||||
import {site, member} from '../../test/fixtures/data';
|
import {member} from '../../test/fixtures/data';
|
||||||
|
|
||||||
const setup = (overrides) => {
|
const setup = (overrides) => {
|
||||||
const mockOnActionFn = jest.fn();
|
|
||||||
const mockSwitchPageFn = jest.fn();
|
|
||||||
const freeMember = member.free;
|
const freeMember = member.free;
|
||||||
const utils = render(
|
const {mockOnActionFn, ...utils} = render(
|
||||||
<AccountHomePage data={{site, member: freeMember}} onAction={mockOnActionFn} switchPage={mockSwitchPageFn} />
|
<AccountHomePage />
|
||||||
);
|
);
|
||||||
const memberEmail = utils.getByText(freeMember.email);
|
const memberEmail = utils.getByText(freeMember.email);
|
||||||
const logoutButton = utils.queryByRole('button', {name: 'Log out'});
|
const logoutButton = utils.queryByRole('button', {name: 'Log out'});
|
||||||
@ -16,7 +14,6 @@ const setup = (overrides) => {
|
|||||||
memberEmail,
|
memberEmail,
|
||||||
logoutButton,
|
logoutButton,
|
||||||
mockOnActionFn,
|
mockOnActionFn,
|
||||||
mockSwitchPageFn,
|
|
||||||
...utils
|
...utils
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import ActionButton from '../common/ActionButton';
|
import ActionButton from '../common/ActionButton';
|
||||||
|
import {ParentContext} from '../ParentContext';
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
export default class MagicLinkPage extends React.Component {
|
export default class MagicLinkPage extends React.Component {
|
||||||
|
static contextType = ParentContext;
|
||||||
|
|
||||||
renderFormHeader() {
|
renderFormHeader() {
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '18px'}}>
|
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '18px'}}>
|
||||||
@ -14,13 +17,13 @@ export default class MagicLinkPage extends React.Component {
|
|||||||
renderLoginMessage() {
|
renderLoginMessage() {
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', justifyContent: 'center'}}>
|
<div style={{display: 'flex', justifyContent: 'center'}}>
|
||||||
<div style={{color: '#3db0ef', fontWeight: 'bold', cursor: 'pointer'}} onClick={() => this.props.switchPage('signin')}> Back to Log in </div>
|
<div style={{color: '#3db0ef', fontWeight: 'bold', cursor: 'pointer'}} onClick={() => this.context.onAction('switchPage', 'signin')}> Back to Log in </div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClose(e) {
|
handleClose(e) {
|
||||||
this.props.onAction('closePopup');
|
this.context.onAction('closePopup');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCloseButton() {
|
renderCloseButton() {
|
||||||
@ -28,7 +31,7 @@ export default class MagicLinkPage extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={e => this.handleSignin(e)}
|
onClick={e => this.handleSignin(e)}
|
||||||
brandColor={this.props.brandColor}
|
brandColor={this.context.brandColor}
|
||||||
label={label}
|
label={label}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,19 +1,11 @@
|
|||||||
import ActionButton from '../common/ActionButton';
|
import ActionButton from '../common/ActionButton';
|
||||||
import InputField from '../common/InputField';
|
import InputField from '../common/InputField';
|
||||||
|
import {ParentContext} from '../ParentContext';
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
|
||||||
|
|
||||||
export default class SigninPage extends React.Component {
|
export default class SigninPage extends React.Component {
|
||||||
static propTypes = {
|
static contextType = ParentContext;
|
||||||
data: PropTypes.shape({
|
|
||||||
site: PropTypes.shape({
|
|
||||||
title: PropTypes.string,
|
|
||||||
description: PropTypes.string
|
|
||||||
}).isRequired
|
|
||||||
}).isRequired,
|
|
||||||
onAction: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -26,7 +18,7 @@ export default class SigninPage extends React.Component {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const email = this.state.email;
|
const email = this.state.email;
|
||||||
|
|
||||||
this.props.onAction('signin', {email});
|
this.context.onAction('signin', {email});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInput(e, field) {
|
handleInput(e, field) {
|
||||||
@ -38,14 +30,14 @@ export default class SigninPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSubmitButton() {
|
renderSubmitButton() {
|
||||||
const isRunning = this.props.action && this.props.action.name === 'signin' && this.props.action.isRunning;
|
const isRunning = (this.context.action === 'signin:running');
|
||||||
const label = this.state.isLoading ? 'Sending' : 'Send Login Link';
|
const label = isRunning ? 'Sending' : 'Send Login Link';
|
||||||
const disabled = isRunning ? true : false;
|
const disabled = isRunning ? true : false;
|
||||||
return (
|
return (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={e => this.handleSignin(e)}
|
onClick={e => this.handleSignin(e)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
brandColor={this.props.brandColor}
|
brandColor={this.context.brandColor}
|
||||||
label={label}
|
label={label}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -75,11 +67,11 @@ export default class SigninPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSignupMessage() {
|
renderSignupMessage() {
|
||||||
const color = this.props.brandColor || '#3db0ef';
|
const brandColor = this.context.brandColor;
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', justifyContent: 'center'}}>
|
<div style={{display: 'flex', justifyContent: 'center'}}>
|
||||||
<div style={{marginRight: '6px', color: '#929292'}}> Don't have an account ? </div>
|
<div style={{marginRight: '6px', color: '#929292'}}> Don't have an account ? </div>
|
||||||
<div style={{color, fontWeight: 'bold', cursor: 'pointer'}} role="button" onClick={() => this.props.switchPage('signup')}> Subscribe </div>
|
<div style={{color: brandColor, fontWeight: 'bold', cursor: 'pointer'}} role="button" onClick={() => this.context.onAction('switchPage', 'signup')}> Subscribe </div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -95,7 +87,7 @@ export default class SigninPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSiteLogo() {
|
renderSiteLogo() {
|
||||||
const siteLogo = (this.props.data.site && this.props.data.site.logo);
|
const siteLogo = this.context.site.logo;
|
||||||
|
|
||||||
const logoStyle = {
|
const logoStyle = {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@ -119,7 +111,7 @@ export default class SigninPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderFormHeader() {
|
renderFormHeader() {
|
||||||
const siteTitle = (this.props.data.site && this.props.data.site.title) || 'Site Title';
|
const siteTitle = this.context.site.title || 'Site Title';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '18px'}}>
|
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '18px'}}>
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {render, fireEvent} from '@testing-library/react';
|
import {render, fireEvent} from '../../utils/tests';
|
||||||
import SigninPage from './SigninPage';
|
import SigninPage from './SigninPage';
|
||||||
import {site} from '../../test/fixtures/data';
|
|
||||||
|
|
||||||
const setup = (overrides) => {
|
const setup = (overrides) => {
|
||||||
const mockOnActionFn = jest.fn();
|
const {mockOnActionFn, ...utils} = render(
|
||||||
const mockSwitchPageFn = jest.fn();
|
<SigninPage />
|
||||||
|
|
||||||
const utils = render(
|
|
||||||
<SigninPage data={{site}} onAction={mockOnActionFn} switchPage={mockSwitchPageFn} />
|
|
||||||
);
|
);
|
||||||
const emailInput = utils.getByLabelText(/email/i);
|
const emailInput = utils.getByLabelText(/email/i);
|
||||||
const submitButton = utils.queryByRole('button', {name: 'Send Login Link'});
|
const submitButton = utils.queryByRole('button', {name: 'Send Login Link'});
|
||||||
@ -18,7 +14,6 @@ const setup = (overrides) => {
|
|||||||
submitButton,
|
submitButton,
|
||||||
signupButton,
|
signupButton,
|
||||||
mockOnActionFn,
|
mockOnActionFn,
|
||||||
mockSwitchPageFn,
|
|
||||||
...utils
|
...utils
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -43,9 +38,9 @@ describe('SigninPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('can call swithPage for signup', () => {
|
test('can call swithPage for signup', () => {
|
||||||
const {signupButton, mockSwitchPageFn} = setup();
|
const {signupButton, mockOnActionFn} = setup();
|
||||||
|
|
||||||
fireEvent.click(signupButton);
|
fireEvent.click(signupButton);
|
||||||
expect(mockSwitchPageFn).toHaveBeenCalledWith('signup');
|
expect(mockOnActionFn).toHaveBeenCalledWith('switchPage', 'signup');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,57 +1,46 @@
|
|||||||
import ActionButton from '../common/ActionButton';
|
import ActionButton from '../common/ActionButton';
|
||||||
import InputField from '../common/InputField';
|
import InputField from '../common/InputField';
|
||||||
|
import {ParentContext} from '../ParentContext';
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
|
||||||
|
|
||||||
export default class SignupPage extends React.Component {
|
class SignupPage extends React.Component {
|
||||||
static propTypes = {
|
static contextType = ParentContext;
|
||||||
data: PropTypes.shape({
|
|
||||||
site: PropTypes.shape({
|
|
||||||
title: PropTypes.string,
|
|
||||||
description: PropTypes.string
|
|
||||||
}).isRequired
|
|
||||||
}).isRequired,
|
|
||||||
onAction: PropTypes.func.isRequired,
|
|
||||||
switchPage: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
plan: 'FREE',
|
plan: 'FREE'
|
||||||
isLoading: false,
|
|
||||||
showSuccess: false
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSignup(e) {
|
handleSignup(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const {onAction} = this.context;
|
||||||
const email = this.state.email;
|
const email = this.state.email;
|
||||||
const name = this.state.name;
|
const name = this.state.name;
|
||||||
const plan = this.state.plan;
|
const plan = this.state.plan;
|
||||||
this.props.onAction('signup', {name, email, plan});
|
onAction('signup', {name, email, plan});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInput(e, field) {
|
handleInput(e, field) {
|
||||||
this.setState({
|
this.setState({
|
||||||
[field]: e.target.value,
|
[field]: e.target.value
|
||||||
showSuccess: false,
|
|
||||||
isLoading: false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSubmitButton() {
|
renderSubmitButton() {
|
||||||
const isRunning = this.props.action && this.props.action.name === 'signup' && this.props.action.isRunning;
|
const {action, brandColor} = this.context;
|
||||||
const label = this.state.isLoading ? 'Sending...' : 'Continue';
|
|
||||||
const disabled = isRunning ? true : false;
|
const label = (action === 'signup:running') ? 'Sending...' : 'Continue';
|
||||||
|
const disabled = (action === 'signup:running') ? true : false;
|
||||||
return (
|
return (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={e => this.handleSignup(e)}
|
onClick={e => this.handleSignup(e)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
brandColor={this.props.brandColor}
|
brandColor={brandColor}
|
||||||
label={label}
|
label={label}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -161,7 +150,8 @@ export default class SignupPage extends React.Component {
|
|||||||
borderRadius: '9px',
|
borderRadius: '9px',
|
||||||
marginBottom: '12px'
|
marginBottom: '12px'
|
||||||
};
|
};
|
||||||
const plans = this.props.data.site && this.props.data.site.plans;
|
let {site} = this.context;
|
||||||
|
const plans = site.plans;
|
||||||
if (!plans) {
|
if (!plans) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -210,11 +200,11 @@ export default class SignupPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderLoginMessage() {
|
renderLoginMessage() {
|
||||||
const color = this.props.brandColor || '#3db0ef';
|
const {brandColor, onAction} = this.context;
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', justifyContent: 'center'}}>
|
<div style={{display: 'flex', justifyContent: 'center'}}>
|
||||||
<div style={{marginRight: '6px', color: '#929292'}}> Already a member ? </div>
|
<div style={{marginRight: '6px', color: '#929292'}}> Already a member ? </div>
|
||||||
<div style={{color, fontWeight: 'bold', cursor: 'pointer'}} role="button" onClick={() => this.props.switchPage('signin')}> Log in </div>
|
<div style={{color: brandColor, fontWeight: 'bold', cursor: 'pointer'}} role="button" onClick={() => onAction('switchPage', 'signin')}> Log in </div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -232,7 +222,8 @@ export default class SignupPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSiteLogo() {
|
renderSiteLogo() {
|
||||||
const siteLogo = (this.props.data.site && this.props.data.site.logo);
|
const {site} = this.context;
|
||||||
|
const siteLogo = site.logo;
|
||||||
|
|
||||||
const logoStyle = {
|
const logoStyle = {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@ -256,7 +247,8 @@ export default class SignupPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderFormHeader() {
|
renderFormHeader() {
|
||||||
const siteTitle = (this.props.data.site && this.props.data.site.title) || 'Site Title';
|
const {site} = this.context;
|
||||||
|
const siteTitle = site.title || 'Site Title';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '18px'}}>
|
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '18px'}}>
|
||||||
@ -277,3 +269,5 @@ export default class SignupPage extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default SignupPage;
|
@ -1,14 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {render, fireEvent} from '@testing-library/react';
|
|
||||||
import SignupPage from './SignupPage';
|
import SignupPage from './SignupPage';
|
||||||
import {site} from '../../test/fixtures/data';
|
import {render, fireEvent} from '../../utils/tests';
|
||||||
|
|
||||||
const setup = (overrides) => {
|
const setup = (overrides) => {
|
||||||
const mockOnActionFn = jest.fn();
|
const {mockOnActionFn, ...utils} = render(
|
||||||
const mockSwitchPageFn = jest.fn();
|
<SignupPage />
|
||||||
|
|
||||||
const utils = render(
|
|
||||||
<SignupPage data={{site}} onAction={mockOnActionFn} switchPage={mockSwitchPageFn} />
|
|
||||||
);
|
);
|
||||||
const emailInput = utils.getByLabelText(/email/i);
|
const emailInput = utils.getByLabelText(/email/i);
|
||||||
const nameInput = utils.getByLabelText(/name/i);
|
const nameInput = utils.getByLabelText(/name/i);
|
||||||
@ -20,7 +16,6 @@ const setup = (overrides) => {
|
|||||||
submitButton,
|
submitButton,
|
||||||
signinButton,
|
signinButton,
|
||||||
mockOnActionFn,
|
mockOnActionFn,
|
||||||
mockSwitchPageFn,
|
|
||||||
...utils
|
...utils
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -51,9 +46,9 @@ describe('SignupPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('can call swithPage for signin', () => {
|
test('can call swithPage for signin', () => {
|
||||||
const {signinButton, mockSwitchPageFn} = setup();
|
const {signinButton, mockOnActionFn} = setup();
|
||||||
|
|
||||||
fireEvent.click(signinButton);
|
fireEvent.click(signinButton);
|
||||||
expect(mockSwitchPageFn).toHaveBeenCalledWith('signin');
|
expect(mockOnActionFn).toHaveBeenCalledWith('switchPage', 'signin');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
40
ghost/portal/src/utils/tests.js
Normal file
40
ghost/portal/src/utils/tests.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Common test setup util - Ref: https://testing-library.com/docs/react-testing-library/setup#custom-render
|
||||||
|
import React from 'react';
|
||||||
|
import {render} from '@testing-library/react';
|
||||||
|
import {ParentContext} from '../components/ParentContext';
|
||||||
|
import {site, member} from '../test/fixtures/data';
|
||||||
|
|
||||||
|
const setupProvider = (context) => {
|
||||||
|
return ({children}) => {
|
||||||
|
return (
|
||||||
|
<ParentContext.Provider value={context}>
|
||||||
|
{children}
|
||||||
|
</ParentContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const customRender = (ui, {options = {}, overrideContext = {}} = {}) => {
|
||||||
|
const mockOnActionFn = jest.fn();
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
site,
|
||||||
|
member: member.free,
|
||||||
|
action: 'init:success',
|
||||||
|
brandColor: site.brand.primaryColor,
|
||||||
|
page: 'signup',
|
||||||
|
onAction: mockOnActionFn,
|
||||||
|
...overrideContext
|
||||||
|
};
|
||||||
|
const utils = render(ui, {wrapper: setupProvider(context), ...options});
|
||||||
|
return {
|
||||||
|
...utils,
|
||||||
|
mockOnActionFn
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// re-export everything
|
||||||
|
export * from '@testing-library/react';
|
||||||
|
|
||||||
|
// override render method
|
||||||
|
export {customRender as render};
|
Loading…
Reference in New Issue
Block a user