mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-17 23:44:39 -05:00
Updated notification handling
refs https://github.com/TryGhost/Team/issues/393 - Updated popup notification handling flow for action success and errors - Updated global notification handling with show popup - Updated action handler to create popup notifications - Removed inline newsletter update message - Generic cleanup of unused code
This commit is contained in:
parent
8ea26be5bb
commit
5cb1f02109
8 changed files with 238 additions and 56 deletions
|
@ -328,7 +328,7 @@ export default class App extends React.Component {
|
|||
|
||||
/**Get final App level context from data/state*/
|
||||
getContextFromState() {
|
||||
const {site, member, action, page, lastPage, showPopup} = this.state;
|
||||
const {site, member, action, page, lastPage, showPopup, popupNotification} = this.state;
|
||||
const contextPage = this.getContextPage({page, member});
|
||||
const contextMember = this.getContextMember({page: contextPage, member});
|
||||
return {
|
||||
|
@ -339,6 +339,7 @@ export default class App extends React.Component {
|
|||
member: contextMember,
|
||||
lastPage,
|
||||
showPopup,
|
||||
popupNotification,
|
||||
onAction: (_action, data) => this.onAction(_action, data)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
function createPopupNotification({type, status, autoHide, closeable, state}) {
|
||||
let count = 0;
|
||||
if (state.popupNotification) {
|
||||
count = (state.popupNotification.count || 0) + 1;
|
||||
}
|
||||
return {
|
||||
type,
|
||||
status,
|
||||
autoHide,
|
||||
closeable,
|
||||
count
|
||||
};
|
||||
}
|
||||
|
||||
function switchPage({data}) {
|
||||
return {
|
||||
page: data.page,
|
||||
|
@ -32,6 +46,7 @@ function closePopup({state}) {
|
|||
return {
|
||||
showPopup: false,
|
||||
lastPage: null,
|
||||
popupNotification: null,
|
||||
page: state.page === 'magiclink' ? '' : state.page
|
||||
};
|
||||
}
|
||||
|
@ -57,29 +72,71 @@ async function signout({api}) {
|
|||
}
|
||||
|
||||
async function signin({data, api}) {
|
||||
await api.member.sendMagicLink(data);
|
||||
return {
|
||||
page: 'magiclink'
|
||||
};
|
||||
try {
|
||||
await api.member.sendMagicLink(data);
|
||||
return {
|
||||
page: 'magiclink'
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
popupNotification: createPopupNotification({
|
||||
type: 'signin:failed',
|
||||
autoHide: false,
|
||||
closeable: true,
|
||||
status: 'error',
|
||||
meta: {
|
||||
reason: e.message
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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});
|
||||
try {
|
||||
const {plan, email, name} = data;
|
||||
if (plan.toLowerCase() === 'free') {
|
||||
await api.member.sendMagicLink(data);
|
||||
} else {
|
||||
await api.member.checkoutPlan({plan, email, name});
|
||||
}
|
||||
return {
|
||||
page: 'magiclink'
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
popupNotification: createPopupNotification({
|
||||
type: 'signup:failed',
|
||||
autoHide: false,
|
||||
closeable: true,
|
||||
status: 'error',
|
||||
meta: {
|
||||
reason: e.message
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
return {
|
||||
page: 'magiclink'
|
||||
};
|
||||
}
|
||||
|
||||
async function updateEmail({data, api}) {
|
||||
await api.member.sendMagicLink(data);
|
||||
return {
|
||||
action: 'updateEmail:success'
|
||||
};
|
||||
try {
|
||||
await api.member.sendMagicLink(data);
|
||||
return {
|
||||
action: 'updateEmail:success'
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
popupNotification: createPopupNotification({
|
||||
type: 'updateEmail:failed',
|
||||
autoHide: false,
|
||||
closeable: true,
|
||||
status: 'error',
|
||||
meta: {
|
||||
reason: e.message
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function checkoutPlan({data, api}) {
|
||||
|
@ -89,14 +146,21 @@ async function checkoutPlan({data, api}) {
|
|||
});
|
||||
}
|
||||
|
||||
async function updateSubscription({data, api}) {
|
||||
async function updateSubscription({data, state, api}) {
|
||||
const {plan, subscriptionId, cancelAtPeriodEnd} = data;
|
||||
await api.member.updateSubscription({
|
||||
planName: plan, subscriptionId, cancelAtPeriodEnd
|
||||
});
|
||||
const member = await api.member.sessionData();
|
||||
const action = 'updateSubscription:success';
|
||||
return {
|
||||
action: 'updateSubscription:success',
|
||||
action,
|
||||
popupNotification: createPopupNotification({
|
||||
type: action,
|
||||
autoHide: true,
|
||||
closeable: true,
|
||||
state
|
||||
}),
|
||||
page: 'accountHome',
|
||||
member: member
|
||||
};
|
||||
|
@ -119,22 +183,13 @@ async function editBilling({data, api}) {
|
|||
await api.member.editBilling();
|
||||
}
|
||||
|
||||
async function updateMember({data, 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
|
||||
};
|
||||
}
|
||||
async function clearPopupNotification() {
|
||||
return {
|
||||
popupNotification: null
|
||||
};
|
||||
}
|
||||
|
||||
async function updateNewsletter({data, api}) {
|
||||
async function updateNewsletter({data, state, api}) {
|
||||
const {subscribed} = data;
|
||||
const member = await api.member.update({subscribed});
|
||||
if (!member) {
|
||||
|
@ -142,25 +197,48 @@ async function updateNewsletter({data, api}) {
|
|||
action: 'updateNewsletter:failed'
|
||||
};
|
||||
} else {
|
||||
const action = 'updateNewsletter:success';
|
||||
return {
|
||||
action: 'updateNewsletter:success',
|
||||
member: member
|
||||
action,
|
||||
member: member,
|
||||
popupNotification: createPopupNotification({
|
||||
type: action,
|
||||
autoHide: true,
|
||||
closeable: true,
|
||||
state
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile({data, api}) {
|
||||
async function updateProfile({data, state, api}) {
|
||||
const {name, subscribed} = data;
|
||||
const member = await api.member.update({name, subscribed});
|
||||
if (!member) {
|
||||
const action = 'updateProfile:failed';
|
||||
return {
|
||||
action: 'updateProfile:failed'
|
||||
action,
|
||||
popupNotification: createPopupNotification({
|
||||
type: action,
|
||||
autoHide: true,
|
||||
closeable: true,
|
||||
status: 'error',
|
||||
state
|
||||
})
|
||||
};
|
||||
} else {
|
||||
const action = 'updateProfile:success';
|
||||
return {
|
||||
action: 'updateProfile:success',
|
||||
action,
|
||||
member: member,
|
||||
page: 'accountHome'
|
||||
page: 'accountHome',
|
||||
popupNotification: createPopupNotification({
|
||||
type: action,
|
||||
autoHide: true,
|
||||
closeable: true,
|
||||
status: 'success',
|
||||
state
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -179,18 +257,18 @@ const Actions = {
|
|||
updateEmail,
|
||||
updateSubscription,
|
||||
cancelSubscription,
|
||||
updateMember,
|
||||
updateNewsletter,
|
||||
updateProfile,
|
||||
clearPopupNotification,
|
||||
editBilling,
|
||||
checkoutPlan
|
||||
};
|
||||
|
||||
/** Handle actions in the App, returns updated state */
|
||||
export default async function ActionHandler({action, data, updateState, state, api}) {
|
||||
export default async function ActionHandler({action, data, state, api}) {
|
||||
const handler = Actions[action];
|
||||
if (handler) {
|
||||
return await handler({data, updateState, state, api}) || {};
|
||||
return await handler({data, state, api}) || {};
|
||||
}
|
||||
return {};
|
||||
}
|
|
@ -80,9 +80,23 @@ class NotificationContent extends React.Component {
|
|||
this.props.onHideNotification();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {showPopup} = this.context;
|
||||
if (!this.state.className && showPopup) {
|
||||
this.setState({
|
||||
className: 'slideout'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {autoHide, duration = 2000} = this.props;
|
||||
if (autoHide) {
|
||||
const {showPopup} = this.context;
|
||||
if (showPopup) {
|
||||
this.setState({
|
||||
className: 'slideout'
|
||||
});
|
||||
} else if (autoHide) {
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
className: 'slideout'
|
||||
|
|
|
@ -72,6 +72,16 @@ class PopupContent extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderPopupNotification() {
|
||||
const {popupNotification} = this.context;
|
||||
if (!popupNotification || !popupNotification.type) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<PopupNotification />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {page, site} = this.context;
|
||||
const {portal_plans: portalPlans} = site;
|
||||
|
@ -90,7 +100,7 @@ class PopupContent extends React.Component {
|
|||
return (
|
||||
<div className='gh-portal-popup-wrapper'>
|
||||
<div className={(hasMode(['preview', 'dev']) ? 'gh-portal-popup-container preview' : 'gh-portal-popup-container') + ' ' + popupWidthStyle} style={pageStyle} ref={node => (this.node = node)} tabIndex="-1">
|
||||
{/* <PopupNotification /> */}
|
||||
{this.renderPopupNotification()}
|
||||
{this.renderActivePage()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -46,7 +46,7 @@ export const PopupNotificationStyles = `
|
|||
transition: all 0.2s ease-in-out forwards;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
.gh-portal-popupnotification .closeicon:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
@ -72,13 +72,99 @@ export const PopupNotificationStyles = `
|
|||
}
|
||||
`;
|
||||
|
||||
const CloseButton = ({hide = false}) => {
|
||||
if (hide) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<CloseIcon className='closeicon' alt='Close' />
|
||||
);
|
||||
};
|
||||
|
||||
const NotificationText = ({type, status}) => {
|
||||
if (type === 'updateNewsletter:success') {
|
||||
return (
|
||||
<p> Newsletter updated! </p>
|
||||
);
|
||||
} else if (type === 'updateSubscription:success') {
|
||||
return (
|
||||
<p> Subscription updated! </p>
|
||||
);
|
||||
} else if (type === 'updateProfile:success') {
|
||||
return (
|
||||
<p> Profile Updated! </p>
|
||||
);
|
||||
} else if (type === 'updateProfile:failed') {
|
||||
return (
|
||||
<p> Failed to update profile! </p>
|
||||
);
|
||||
}
|
||||
const label = status === 'success' ? 'Success' : 'Failed';
|
||||
return (
|
||||
<p> ${label}</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default class PopupNotification extends React.Component {
|
||||
static contextType = AppContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
className: '',
|
||||
notificationType: ''
|
||||
};
|
||||
}
|
||||
|
||||
onAnimationEnd(e) {
|
||||
if (e.animationName === 'popupnotification-slideout') {
|
||||
this.context.onAction('clearPopupNotification');
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {popupNotification} = this.context;
|
||||
if (popupNotification.count !== this.state.count) {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.handlePopupNotification({popupNotification});
|
||||
}
|
||||
}
|
||||
|
||||
handlePopupNotification({popupNotification}) {
|
||||
if (popupNotification.autoHide) {
|
||||
const {duration = 2000} = popupNotification;
|
||||
this.timeoutId = setTimeout(() => {
|
||||
this.setState({
|
||||
className: 'slideout',
|
||||
notificationCount: popupNotification.count
|
||||
});
|
||||
}, duration);
|
||||
} else {
|
||||
this.setState({
|
||||
notificationCount: popupNotification.count
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {popupNotification} = this.context;
|
||||
this.handlePopupNotification({popupNotification});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {popupNotification} = this.context;
|
||||
const {className} = this.state;
|
||||
const {type, status, closeable} = popupNotification;
|
||||
const statusClass = status ? ` ${status}` : '';
|
||||
const slideClass = className ? ` ${className}` : '';
|
||||
|
||||
return (
|
||||
<div className='gh-portal-popupnotification success'>
|
||||
<p>Plan changed successfully</p>
|
||||
<CloseIcon className='closeicon' alt='Close' />
|
||||
<div className={`gh-portal-popupnotification${statusClass}${slideClass}`} onAnimationEnd={e => this.onAnimationEnd(e)}>
|
||||
<NotificationText type={type} status={status} />
|
||||
<CloseButton hide={!closeable} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -196,9 +196,6 @@ const AccountActions = ({member, site, action, openEditProfile, openUpdatePlan,
|
|||
const {name, email, subscribed} = member;
|
||||
|
||||
let label = subscribed ? 'Subscribed to email newsletters' : 'Not subscribed to email newsletters';
|
||||
if (action === 'updateNewsletter:success') {
|
||||
label = '✓ Newsletter updated';
|
||||
}
|
||||
return (
|
||||
<div className='gh-portal-list'>
|
||||
<section>
|
||||
|
|
|
@ -186,10 +186,6 @@ export default class AccountProfilePage extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
onToggleSubscription(e, subscribed) {
|
||||
this.context.onAction('updateMember', {subscribed: !subscribed});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {member} = this.context;
|
||||
if (!member) {
|
||||
|
|
|
@ -104,7 +104,7 @@ function setupGhostApi({siteUrl = window.location.origin}) {
|
|||
if (res.ok) {
|
||||
return 'Success';
|
||||
} else {
|
||||
return 'Failed to send magic link';
|
||||
throw new Error('Failed to send magic link email');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue