Refactored app setup to more modular structure

no issue

- Adds new mode check util for dev, preview and test, cleaned up manual checks
- Extracts Pages setup to its own file allowing common use
- Extracts action handling to its own file for neater structure
- Breaks up main App.js into proper methods for data fetching across usecases
- Adds better handling for App context through state
- Fixed trigger button test
This commit is contained in:
Rish 2020-07-21 10:52:31 +05:30
parent 47e7b86637
commit 0e30b63f62
14 changed files with 465 additions and 361 deletions

View File

@ -2,9 +2,11 @@ import TriggerButton from './components/TriggerButton';
import PopupModal from './components/PopupModal';
import setupGhostApi from './utils/api';
import AppContext from './AppContext';
import {hasMode} from './utils/check-mode';
import {getActivePage, isAccountPage} from './pages';
import * as Fixtures from './utils/fixtures';
import ActionHandler from './actions';
import './App.css';
const React = require('react');
export default class App extends React.Component {
@ -41,6 +43,27 @@ export default class App extends React.Component {
});
}
/** Setup custom trigger buttons handling on page */
setupCustomTriggerButton() {
// Handler for custom buttons
this.clickHandler = (event) => {
const target = event.currentTarget;
const pagePath = (target && target.dataset.portal);
const pageFromPath = this.getPageFromPath(pagePath);
event.preventDefault();
this.onAction('openPopup', {page: pageFromPath});
};
const customTriggerSelector = '[data-portal]';
const popupCloseClass = 'gh-members-popup-close';
this.customTriggerButtons = document.querySelectorAll(customTriggerSelector) || [];
this.customTriggerButtons.forEach((customTriggerButton) => {
customTriggerButton.classList.add(popupCloseClass);
customTriggerButton.addEventListener('click', this.clickHandler);
});
}
/** Handle portal class set on custom trigger buttons */
handleCustomTriggerClassUpdate() {
const popupOpenClass = 'gh-members-popup-open';
const popupCloseClass = 'gh-members-popup-close';
@ -52,103 +75,202 @@ export default class App extends React.Component {
});
}
getStripeUrlParam() {
const url = new URL(window.location);
return url.searchParams.get('stripe');
/** Initialize portal setup on load, fetch data and setup state*/
async initSetup() {
try {
// Fetch data from API, links, preview, dev sources
const {site, member, page, showPopup} = await this.fetchData();
this.setState({
site,
member,
page,
showPopup,
action: 'init:success',
initStatus: 'success'
});
// Listen to preview mode changes
this.hashHandler = () => {
this.updateStateForPreview();
};
window.addEventListener('hashchange', this.hashHandler, false);
} catch (e) {
/* eslint-disable no-console */
console.error(`[Portal] Failed to initialize:`, e);
/* eslint-enable no-console */
this.setState({
action: 'init:failed',
initStatus: 'failed'
});
}
}
getDefaultPage({member = this.state.member, stripeParam} = {}) {
// Loads default page and popup state for local UI testing
if (process.env.NODE_ENV === 'development') {
return {
page: 'accountHome',
showPopup: true
};
}
/** Fetch data from all available sources */
async fetchData() {
const {site: apiSiteData, member} = await this.fetchApiData();
const {site: devSiteData, ...restDevData} = this.fetchDevData();
const {site: linkSiteData, ...restLinkData} = this.fetchLinkData();
const {site: previewSiteData, ...restPreviewData} = this.fetchPreviewData();
const stripeParam = this.getStripeUrlParam();
let page = '';
/** Set page for magic link popup on stripe success*/
if (!member && stripeParam === 'success') {
return {page: 'magiclink', showPopup: true};
page = 'magiclink';
}
if (member) {
return {
member,
page,
site: {
...apiSiteData,
...linkSiteData,
...previewSiteData,
...devSiteData
},
...restDevData,
...restLinkData,
...restPreviewData
};
}
/** Fetch state for Dev mode */
fetchDevData() {
// Setup custom dev mode data from fixtures
if (hasMode(['dev'])) {
return {
showPopup: true,
site: Fixtures.site,
member: Fixtures.member.free,
page: 'accountHome'
};
}
return {
page: 'signup'
};
return {};
}
updateStateForPreview() {
const {site: previewSite, ...restPreview} = this.getPreviewState();
this.setState({
site: {
...this.state.site,
...(previewSite || {})
},
member: this.getPreviewMember(this.state.member),
...restPreview
});
}
getStateFromQueryString(qs = '') {
const previewState = {
/** Fetch state from Query String */
fetchQueryStrData(qs = '') {
const qsParams = new URLSearchParams(qs);
const data = {
site: {}
};
const allowedPlans = [];
const qsParams = new URLSearchParams(qs);
// Handle the key/value pairs
// Handle the query params key/value pairs
for (let pair of qsParams.entries()) {
const key = pair[0];
const value = decodeURIComponent(pair[1]);
if (key === 'button') {
previewState.site.portal_button = JSON.parse(value);
data.site.portal_button = JSON.parse(value);
} else if (key === 'name') {
previewState.site.portal_name = JSON.parse(value);
data.site.portal_name = JSON.parse(value);
} else if (key === 'isFree' && JSON.parse(value)) {
allowedPlans.push('free');
} else if (key === 'isMonthly' && JSON.parse(value)) {
allowedPlans.push('monthly');
} else if (key === 'isYearly' && JSON.parse(value)) {
allowedPlans.push('yearly');
} else if (key === 'page') {
previewState.page = value;
} else if (key === 'accentColor') {
previewState.site.accent_color = value;
} else if (key === 'buttonIcon') {
previewState.site.portal_button_icon = value;
} else if (key === 'plans') {
data.site.portal_plans = value ? value.split(',') : [];
} else if (key === 'page' && value) {
data.page = value;
} else if (key === 'accentColor' && value) {
data.site.accent_color = value;
} else if (key === 'buttonIcon' && value) {
data.site.portal_button_icon = value;
} else if (key === 'signupButtonText') {
previewState.site.portal_button_signup_text = value;
} else if (key === 'buttonStyle') {
previewState.site.portal_button_style = value;
data.site.portal_button_signup_text = value || '';
} else if (key === 'buttonStyle' && value) {
data.site.portal_button_style = value;
}
}
previewState.site.portal_plans = allowedPlans;
previewState.showPopup = true;
return previewState;
data.site.portal_plans = allowedPlans;
return data;
}
getPreviewState() {
const [path, qs] = window.location.hash.substr(1).split('?');
const previewState = {
site: {}
};
if (path.startsWith('/portal')) {
previewState.showPopup = true;
if (qs) {
return this.getStateFromQueryString(qs);
}
if (path.startsWith('/portal/')) {
const pagePath = path.replace('/portal/', '');
const pageFromPath = this.getPageFromPath(pagePath);
if (pageFromPath) {
previewState.page = pageFromPath;
}
}
/** Fetch state from Portal Links */
fetchLinkData() {
const [path] = window.location.hash.substr(1).split('?');
const linkRegex = /^\/portal(?:\/(\w+(?:\/\w+)?))?$/;
if (path && linkRegex.test(path)) {
const [,pagePath] = path.match(linkRegex);
const page = this.getPageFromPath(pagePath);
return {
showPopup: true,
...(page ? {page} : {})
};
}
return previewState;
return {};
}
/** Fetch state from Preview mode */
fetchPreviewData() {
const [, qs] = window.location.hash.substr(1).split('?');
if (hasMode(['preview'])) {
const data = this.fetchQueryStrData(qs);
data.showPopup = true;
return data;
}
return {};
}
/** Fetch site and member session data with Ghost Apis */
async fetchApiData() {
try {
const {siteUrl} = this.props;
this.GhostApi = setupGhostApi({siteUrl});
const {site, member} = await this.GhostApi.init();
return {site, member};
} catch (e) {
if (hasMode(['dev', 'test'])) {
return {};
}
throw e;
}
}
/** Handle actions from across App and update state */
async onAction(action, data) {
this.setState({
action: `${action}:running`
});
try {
const updatedState = await ActionHandler({action, data, state: this.state, api: this.GhostApi});
this.setState(updatedState);
/** Reset action state after short timeout */
setTimeout(() => {
this.setState({
action: ''
});
}, 5000);
} catch (e) {
this.setState({
action: `${action}:failed`
});
}
}
/**Handle state update for preview url changes */
updateStateForPreview() {
const {site: previewSite, ...restPreviewData} = this.fetchPreviewData();
this.setState({
site: {
...this.state.site,
...(previewSite || {})
},
...restPreviewData
});
}
/**Fetch Stripe param from site url after redirect from Stripe page*/
getStripeUrlParam() {
const url = new URL(window.location);
return url.searchParams.get('stripe');
}
/**Get Portal page from Link/Data-attribute path*/
getPageFromPath(path) {
if (path === 'signup') {
return 'signup';
@ -163,236 +285,59 @@ export default class App extends React.Component {
}
}
getPreviewMember(member) {
const [path, qs] = window.location.hash.substr(1).split('?');
if (path === '/portal' && qs) {
const {site: previewSite, ...restPreview} = this.getPreviewState();
if (restPreview.page.includes('account')) {
return member || Fixtures.member.free;
}
return null;
} else if (process.env.NODE_ENV === 'development') {
return member || Fixtures.member.paid;
}
return member;
}
async initSetup() {
const {site, member} = await this.fetchData() || {};
if (!site) {
this.setState({
action: 'init:failed',
initStatus: 'failed'
});
} else {
const stripeParam = this.getStripeUrlParam();
const {page, showPopup = false} = this.getDefaultPage({member, stripeParam});
const {site: previewSite, ...restPreview} = this.getPreviewState();
const initState = {
site: {
...site,
...(previewSite || {})
},
member: this.getPreviewMember(member),
page,
showPopup,
action: 'init:success',
initStatus: 'success',
...restPreview
};
this.setState(initState);
this.hashHandler = () => {
this.updateStateForPreview();
};
window.addEventListener('hashchange', this.hashHandler, false);
}
}
// Fetch site and member session data with Ghost Apis
async fetchData() {
const {siteUrl} = this.props;
try {
this.GhostApi = setupGhostApi({siteUrl});
const {site, member} = await this.GhostApi.init();
return {site, member};
} catch (e) {
/* eslint-disable no-console */
console.error(`[Members.js] Failed to initialize`);
/* eslint-enable no-console */
return null;
}
}
setupCustomTriggerButton() {
// Handler for custom buttons
this.clickHandler = (event) => {
const target = event.currentTarget;
const {page: defaultPage} = this.getDefaultPage();
const pagePath = (target && target.dataset.portal);
const pageFromPath = this.getPageFromPath(pagePath) || defaultPage;
event.preventDefault();
this.onAction('openPopup', {page: pageFromPath});
};
const customTriggerSelector = '[data-portal]';
const popupCloseClass = 'gh-members-popup-close';
this.customTriggerButtons = document.querySelectorAll(customTriggerSelector) || [];
this.customTriggerButtons.forEach((customTriggerButton) => {
customTriggerButton.classList.add(popupCloseClass);
customTriggerButton.addEventListener('click', this.clickHandler);
});
}
getActionData(action) {
const [type, status, reason] = action.split(':');
return {type, status, reason};
}
/**Get Accent color from site data, fallback to default*/
getAccentColor() {
const {accent_color: accentColor = '#3db0ef'} = this.state.site || {};
return accentColor || '#3db0ef';
}
async onAction(action, data) {
this.setState({
action: `${action}:running`
});
try {
if (action === 'switchPage') {
this.setState({
page: data.page,
lastPage: data.lastPage || null
});
} else if (action === 'togglePopup') {
this.setState({
showPopup: !this.state.showPopup
});
} else if (action === 'openPopup') {
this.setState({
showPopup: true,
page: data.page
});
} else if (action === 'back') {
if (this.state.lastPage) {
this.setState({
page: this.state.lastPage
});
}
} else if (action === 'closePopup') {
const {page: defaultPage} = this.getDefaultPage();
this.setState({
showPopup: false,
page: this.state.page === 'magiclink' ? defaultPage : this.state.page
});
} else if (action === 'signout') {
await this.GhostApi.member.signout();
this.setState({
action: 'signout:success'
});
} else if (action === 'signin') {
await this.GhostApi.member.sendMagicLink(data);
this.setState({
action: 'signin:success',
page: 'magiclink'
});
} else if (action === 'signup') {
const {plan, email, name} = data;
if (plan.toLowerCase() === 'free') {
await this.GhostApi.member.sendMagicLink(data);
} else {
await this.GhostApi.member.checkoutPlan({plan, email, name});
}
this.setState({
action: 'signup:success',
page: 'magiclink'
});
} else if (action === 'updateEmail') {
await this.GhostApi.member.sendMagicLink(data);
this.setState({
action: 'updateEmail:success'
});
} else if (action === 'checkoutPlan') {
const {plan} = data;
await this.GhostApi.member.checkoutPlan({
plan
});
} else if (action === 'updateSubscription') {
const {plan, subscriptionId, cancelAtPeriodEnd} = data;
await this.GhostApi.member.updateSubscription({
planName: plan, subscriptionId, cancelAtPeriodEnd
});
const member = await this.GhostApi.member.sessionData();
this.setState({
action: 'updateSubscription:success',
page: 'accountHome',
member: member
});
} else if (action === 'editBilling') {
await this.GhostApi.member.editBilling();
} else if (action === 'updateMember') {
const {name, subscribed} = data;
const member = await this.GhostApi.member.update({name, subscribed});
if (!member) {
this.setState({
action: 'updateMember:failed'
});
} else {
this.setState({
action: 'updateMember:success',
member: member
});
}
/**Get final page set in App context from state data*/
getContextPage({page, member}) {
/**Set default page based on logged-in status */
if (!page) {
page = member ? 'accountHome' : 'signup';
}
return getActivePage({page});
}
/**Get final member set in App context from state data*/
getContextMember({page, member}) {
if (hasMode(['dev', 'preview'])) {
/** Use dummy member(free or paid) for account pages in dev/preview mode*/
if (isAccountPage({page})) {
return member || Fixtures.member.free;
}
setTimeout(() => {
this.setState({
action: ''
});
}, 5000);
} catch (e) {
this.setState({
action: `${action}:failed`
});
/** Ignore member for non-account pages in dev/preview mode*/
return null;
}
return member;
}
renderPopup() {
if (this.state.showPopup) {
return (
<PopupModal />
);
}
return null;
}
renderTriggerButton() {
const {portal_button: portalButton} = this.state.site;
if (portalButton === undefined || portalButton) {
return (
<TriggerButton
isPopupOpen={this.state.showPopup}
/>
);
}
return null;
/**Get final App level context from data/state*/
getContextFromState() {
const {site, member, action, page, lastPage, showPopup} = this.state;
const contextPage = this.getContextPage({page, member});
const contextMember = this.getContextMember({page: contextPage, member});
return {
site,
action,
brandColor: this.getAccentColor(),
page: contextPage,
member: contextMember,
lastPage,
showPopup,
onAction: (_action, data) => this.onAction(_action, data)
};
}
render() {
if (this.state.initStatus === 'success') {
const {site, member, action, page, lastPage} = this.state;
return (
<AppContext.Provider value={{
site,
member,
action,
brandColor: this.getAccentColor(),
page,
lastPage,
onAction: (_action, data) => this.onAction(_action, data)
}}>
{this.renderPopup()}
{this.renderTriggerButton()}
<AppContext.Provider value={this.getContextFromState()}>
<PopupModal />
<TriggerButton />
</AppContext.Provider>
);
}

133
ghost/portal/src/actions.js Normal file
View File

@ -0,0 +1,133 @@
function switchPage({data}) {
return {
page: data.page,
lastPage: data.lastPage || null
};
}
function togglePopup({state}) {
return {
showPopup: !state.showPopup
};
}
function openPopup({data}) {
return {
showPopup: true,
page: data.page
};
}
function back({state}) {
if (state.lastPage) {
return {
page: state.lastPage
};
}
}
function closePopup({state}) {
return {
showPopup: false,
page: state.page === 'magiclink' ? '' : state.page
};
}
async function signout({api}) {
await api.member.signout();
return {
action: 'signout:success'
};
}
async function signin({data, api}) {
await api.member.sendMagicLink(data);
return {
action: 'signin:success',
page: 'magiclink'
};
}
async function signup({data, api}) {
const {plan, email, name} = data;
if (plan.toLowerCase() === 'free') {
await api.member.sendMagicLink(data);
} else {
await api.member.checkoutPlan({plan, email, name});
}
return {
action: 'signup:success',
page: 'magiclink'
};
}
async function updateEmail({data, api}) {
await api.member.sendMagicLink(data);
return {
action: 'updateEmail:success'
};
}
async function checkoutPlan({data, api}) {
const {plan} = data;
await api.member.checkoutPlan({
plan
});
}
async function updateSubscription({data, api}) {
const {plan, subscriptionId, cancelAtPeriodEnd} = data;
await api.member.updateSubscription({
planName: plan, subscriptionId, cancelAtPeriodEnd
});
const member = await api.member.sessionData();
return {
action: 'updateSubscription:success',
page: 'accountHome',
member: member
};
}
async function editBilling({data, updateState, state, api}) {
await api.member.editBilling();
}
async function updateMember({data, updateState, state, api}) {
const {name, subscribed} = data;
const member = await api.member.update({name, subscribed});
if (!member) {
return {
action: 'updateMember:failed'
};
} else {
return {
action: 'updateMember:success',
member: member
};
}
}
const Actions = {
togglePopup,
openPopup,
closePopup,
switchPage,
back,
signout,
signin,
signup,
updateEmail,
updateSubscription,
updateMember,
editBilling,
checkoutPlan
};
/** Handle actions in the App, returns updated state */
export default async function ActionHandler({action, data, updateState, state, api}) {
const handler = Actions[action];
if (handler) {
return await handler({data, updateState, state, api}) || {};
}
return {};
}

View File

@ -1,15 +1,8 @@
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';
import {ReactComponent as CloseIcon} from '../images/icons/close.svg';
import AppContext from '../AppContext';
import FrameStyle from './Frame.styles';
import AccountPlanPage from './pages/AccountPlanPage';
import AccountProfilePage from './pages/AccountProfilePage';
import LinkPage from './pages/LinkPage';
import Pages, {getActivePage} from '../pages';
const React = require('react');
@ -89,17 +82,6 @@ const StylesWrapper = ({member}) => {
};
};
const Pages = {
signin: SigninPage,
signup: SignupPage,
accountHome: AccountHomePage,
accountPlan: AccountPlanPage,
accountProfile: AccountProfilePage,
magiclink: MagicLinkPage,
loading: LoadingPage,
links: LinkPage
};
class PopupContent extends React.Component {
static contextType = AppContext;
@ -127,16 +109,9 @@ class PopupContent extends React.Component {
}
}
getCurrentPage() {
const {page} = this.context;
if (Object.keys(Pages).includes(page)) {
return page;
}
return 'signup';
}
renderCurrentPage() {
renderActivePage() {
const {page} = this.context;
getActivePage({page});
const PageComponent = Pages[page];
return (
@ -153,11 +128,10 @@ class PopupContent extends React.Component {
}
render() {
const page = this.getCurrentPage();
return (
<div className='gh-portal-popup-container' ref={this.container}>
{this.renderPopupClose()}
{this.renderCurrentPage(page)}
{this.renderActivePage()}
</div>
);
}
@ -193,16 +167,6 @@ export default class PopupModal extends React.Component {
);
}
renderPopupContent() {
const page = this.getCurrentPage();
return (
<div className='gh-portal-popup-container'>
{this.renderPopupClose()}
{this.renderCurrentPage(page)}
</div>
);
}
handlePopupClose(e) {
e.preventDefault();
if (e.target === e.currentTarget) {
@ -221,18 +185,10 @@ export default class PopupModal extends React.Component {
);
}
getCurrentPage() {
const {page} = this.context;
if (Object.keys(Pages).includes(page)) {
return page;
}
return 'signup';
}
renderFrameContainer() {
const {member} = this.context;
const Styles = StylesWrapper({member});
const page = this.getCurrentPage();
const page = getActivePage({page: this.context.page});
const frameStyle = {
...Styles.frame.common,
...Styles.frame[page]
@ -252,6 +208,10 @@ export default class PopupModal extends React.Component {
}
render() {
return this.renderFrameContainer();
const {showPopup} = this.context;
if (showPopup) {
return this.renderFrameContainer();
}
return null;
}
}

View File

@ -210,6 +210,13 @@ export default class TriggerButton extends React.Component {
}
render() {
const {portal_button: portalButton} = this.context.site;
const {showPopup} = this.context;
if (!portalButton) {
return null;
}
const hasText = this.hasText();
const Style = Styles({brandColor: this.context.brandColor, hasText});
@ -223,7 +230,7 @@ export default class TriggerButton extends React.Component {
return (
<Frame style={frameStyle} title="membersjs-trigger">
<TriggerButtonContent isPopupOpen={this.props.isPopupOpen} updateWidth={width => this.onWidthChange(width)} />
<TriggerButtonContent isPopupOpen={showPopup} updateWidth={width => this.onWidthChange(width)} />
</Frame>
);
}

View File

@ -1,13 +1,22 @@
import React from 'react';
import {render} from '@testing-library/react';
import {render} from 'test-utils';
import TriggerButton from './TriggerButton';
const setup = (overrides) => {
const {mockOnActionFn, ...utils} = render(
<TriggerButton />
);
const triggerFrame = utils.getByTitle('membersjs-trigger');
return {
triggerFrame,
...utils
};
};
describe('Trigger Button', () => {
test('renders', () => {
const {getByTitle} = render(
<TriggerButton />
);
const triggerFrame = getByTitle('membersjs-trigger');
const {triggerFrame} = setup();
expect(triggerFrame).toBeInTheDocument();
});

View File

@ -2,7 +2,6 @@ import AppContext from '../../AppContext';
import MemberAvatar from '../common/MemberGravatar';
import ActionButton from '../common/ActionButton';
import Switch from '../common/Switch';
import isPreviewMode from '../../utils/is-preview-mode';
const React = require('react');
@ -288,7 +287,7 @@ class PaidAccountHomePage extends React.Component {
</div>
<button className='gh-portal-btn gh-portal-btn-accountdetail' onClick={e => this.openUpdatePlan(e)}>Change</button>
</section>
<section className='gh-portal-accountdetail-section'>
<div className='flex flex-column flex-grow-1'>
<h3 className='gh-portal-setting-heading paid-home'>Billing Info</h3>
@ -298,7 +297,7 @@ class PaidAccountHomePage extends React.Component {
</div>
<button className='gh-portal-btn gh-portal-btn-accountdetail' onClick={e => this.onEditBilling(e)}>Update</button>
</section>
<section className='gh-portal-accountdetail-section'>
<div className='flex flex-column flex-grow-1'>
<h3 className='gh-portal-setting-heading paid-home'>Newsletter</h3>
@ -335,7 +334,7 @@ export default class AccountHomePage extends React.Component {
componentDidMount() {
const {member} = this.context;
if (!member && !isPreviewMode()) {
if (!member) {
this.context.onAction('switchPage', {
page: 'signup'
});

View File

@ -1,7 +1,6 @@
import AppContext from '../../AppContext';
import ActionButton from '../common/ActionButton';
import PlansSection from '../common/PlansSection';
import isPreviewMode from '../../utils/is-preview-mode';
import {ReactComponent as ArrowLeftIcon} from '../../images/icons/arrow-left.svg';
const React = require('react');
@ -20,7 +19,7 @@ export default class AccountPlanPage extends React.Component {
componentDidMount() {
const {member} = this.context;
if (!member && !isPreviewMode()) {
if (!member) {
this.context.onAction('switchPage', {
page: 'signup'
});

View File

@ -3,7 +3,6 @@ import MemberAvatar from '../common/MemberGravatar';
import ActionButton from '../common/ActionButton';
import InputField from '../common/InputField';
import Switch from '../common/Switch';
import isPreviewMode from '../../utils/is-preview-mode';
import {ReactComponent as ArrowLeftIcon} from '../../images/icons/arrow-left.svg';
const React = require('react');
@ -22,7 +21,7 @@ export default class AccountProfilePage extends React.Component {
componentDidMount() {
const {member} = this.context;
if (!member && !isPreviewMode()) {
if (!member) {
this.context.onAction('switchPage', {
page: 'signup'
});

View File

@ -1,7 +1,6 @@
import ActionButton from '../common/ActionButton';
import InputField from '../common/InputField';
import AppContext from '../../AppContext';
import isPreviewMode from '../../utils/is-preview-mode';
const React = require('react');
@ -17,7 +16,7 @@ export default class SigninPage extends React.Component {
componentDidMount() {
const {member} = this.context;
if (member && !isPreviewMode()) {
if (member) {
this.context.onAction('switchPage', {
page: 'accountHome'
});

View File

@ -2,7 +2,6 @@ import ActionButton from '../common/ActionButton';
import InputField from '../common/InputField';
import AppContext from '../../AppContext';
import PlansSection from '../common/PlansSection';
import isPreviewMode from '../../utils/is-preview-mode';
const React = require('react');
@ -20,7 +19,7 @@ class SignupPage extends React.Component {
componentDidMount() {
const {member} = this.context;
if (member && !isPreviewMode()) {
if (member) {
this.context.onAction('switchPage', {
page: 'accountHome'
});

36
ghost/portal/src/pages.js Normal file
View File

@ -0,0 +1,36 @@
import SigninPage from './components/pages/SigninPage';
import SignupPage from './components/pages/SignupPage';
import AccountHomePage from './components/pages/AccountHomePage';
import MagicLinkPage from './components/pages/MagicLinkPage';
import LoadingPage from './components/pages/LoadingPage';
import AccountPlanPage from './components/pages/AccountPlanPage';
import AccountProfilePage from './components/pages/AccountProfilePage';
import LinkPage from './components/pages/LinkPage';
/** List of all available pages in Portal, mapped to their UI component
* Any new page added to portal needs to be mapped here
*/
const Pages = {
signin: SigninPage,
signup: SignupPage,
accountHome: AccountHomePage,
accountPlan: AccountPlanPage,
accountProfile: AccountProfilePage,
magiclink: MagicLinkPage,
loading: LoadingPage,
links: LinkPage
};
/** Return page if valid, fallback to signup */
export const getActivePage = function ({page}) {
if (Object.keys(Pages).includes(page)) {
return page;
}
return 'signup';
};
export const isAccountPage = function ({page}) {
return page.includes('account');
};
export default Pages;

View File

@ -1,4 +1,5 @@
import * as Fixtures from './fixtures';
import {hasMode} from './check-mode';
function setupGhostApi({siteUrl = window.location.origin}) {
const apiPath = 'members/api';
@ -215,11 +216,10 @@ function setupGhostApi({siteUrl = window.location.origin}) {
};
api.init = async () => {
// Load member from fixtures for local development
if (process.env.NODE_ENV === 'development') {
return {site: Fixtures.site, member: null};
}
// // Load site data from fixtures for dev/test modes
// if (hasMode(['dev', 'test'])) {
// return {site: Fixtures.site, member: null};
// }
const {site} = await api.site.read();
const member = await api.member.sessionData();
return {site, member};

View File

@ -0,0 +1,25 @@
export const isPreviewMode = function () {
const [path, qs] = window.location.hash.substr(1).split('?');
return (path === '/portal/preview') || (path === '/portal' && qs);
};
export const isDevMode = function () {
return (process.env.NODE_ENV === 'development');
};
export const isTestMode = function () {
return (process.env.NODE_ENV === 'test');
};
const modeFns = {
preview: isPreviewMode,
dev: isDevMode,
test: isTestMode
};
export const hasMode = (modes = []) => {
return modes.some((mode) => {
const modeFn = modeFns[mode];
return !!(modeFn && modeFn());
});
};

View File

@ -1,6 +0,0 @@
function isPreviewMode() {
const [path, qs] = window.location.hash.substr(1).split('?');
return ((process.env.NODE_ENV === 'development') || (path === '/portal' && qs));
}
export default isPreviewMode;