mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 20:03:12 +03:00
Added mock member details component
Prepring for custom products we'll change the overall layout of member forms. This commit has a component that's loaded when dev flag is on for the new layout with mocked product data.
This commit is contained in:
parent
01c2facccc
commit
96faf27ba8
224
ghost/admin/app/components/gh-member-settings-form-cp.hbs
Normal file
224
ghost/admin/app/components/gh-member-settings-form-cp.hbs
Normal file
@ -0,0 +1,224 @@
|
||||
<div class="gh-member-settings" ...attributes>
|
||||
|
||||
<section class="gh-main-section no-heading gh-member-detail-overview">
|
||||
<div class="gh-main-section-block">
|
||||
<div class="gh-main-section-content">
|
||||
<div class="flex items-center">
|
||||
{{#if (or this.member.name this.member.email)}}
|
||||
<GhMemberAvatar
|
||||
@member={{this.member}}
|
||||
@sizeClass={{if this.member.name 'f-subheadline fw4 lh-zero tracked-1' 'f-headline fw4 lh-zero tracked-1'}}
|
||||
@containerClass="w20 h20 mr4 gh-member-detail-avatar"
|
||||
/>
|
||||
{{else}}
|
||||
<div class="flex items-center justify-center br-100 w18 h18 mr4 gh-new-member-avatar">
|
||||
<span class="gh-member-avatar-label f-subheadline fw4 lh-zero tracked-1">N</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div>
|
||||
<h3>
|
||||
{{or this.member.name this.member.email}}
|
||||
</h3>
|
||||
<p>
|
||||
{{#if (and this.member.name this.member.email)}}
|
||||
<a href="mailto:{{this.member.email}}">{{this.member.email}}</a>
|
||||
{{/if}}
|
||||
</p>
|
||||
{{#unless this.member.isNew}}
|
||||
<p class="{{if this.member.name "nudge-bottom--2"}}">
|
||||
{{#if this.member.geolocation}}
|
||||
{{#if (and (eq this.member.geolocation.country_code "US") @member.geolocation.region)}}
|
||||
{{this.member.geolocation.region}}, US
|
||||
{{else}}
|
||||
{{or this.member.geolocation.country "Unknown location"}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
Unknown location
|
||||
{{/if}}
|
||||
– Created on {{moment-format @member.createdAtUTC "D MMM YYYY"}}
|
||||
</p>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gh-main-section columns-3">
|
||||
<div class="gh-main-section-block span-2">
|
||||
<div class="gh-main-section-content grey">
|
||||
<div>
|
||||
<div class="gh-cp-member-email-name">
|
||||
<GhFormGroup @errors={{this.member.errors}} @hasValidated={{this.member.hasValidated}} @property="name" @classNames="max-width">
|
||||
<label for="member-name">Name</label>
|
||||
<GhTextInput @id="member-name" @name="name" @value={{this.scratchMember.name}} @tabindex="1"
|
||||
@focus-out={{action "setProperty" "name" this.scratchMember.name}} data-test-input="member-name" />
|
||||
<GhErrorMessage @errors={{member.errors}} @property="name" />
|
||||
</GhFormGroup>
|
||||
|
||||
<GhFormGroup @errors={{this.member.errors}} @hasValidated={{this.member.hasValidated}} @property="email" @classNames="max-width">
|
||||
<label for="member-email">Email</label>
|
||||
<GhTextInput @value={{this.scratchMember.email}} @id="member-email" @name="email" @tabindex="2"
|
||||
@autocapitalize="off" @autocorrect="off" @autocomplete="off"
|
||||
@focus-out={{action "setProperty" "email" this.scratchMember.email}} data-test-input="member-email"/>
|
||||
<GhErrorMessage @errors={{this.member.errors}} @property="email" />
|
||||
</GhFormGroup>
|
||||
</div>
|
||||
|
||||
<GhFormGroup @classNames="gh-member-labels">
|
||||
<label for="label-input">Labels</label>
|
||||
<GhMemberLabelInput @onChange={{action "setLabels"}} @labels={{this.member.labels}} @triggerId="label-input" data-test-input="" />
|
||||
</GhFormGroup>
|
||||
|
||||
<GhFormGroup @errors={{this.member.errors}} @hasValidated={{this.member.hasValidated}} @property="note" @classNames="mb0 gh-member-note">
|
||||
<label for="member-note">Note <span class="midgrey-l2 fw4">(not visible to member)</span></label>
|
||||
<GhTextarea @id="member-note" @name="note" @class="gh-member-details-textarea" @tabindex="3"
|
||||
@value={{this.scratchMember.note}} @focus-out={{action "setProperty" "note" this.scratchMember.note}} data-test-input="member-note" />
|
||||
<GhErrorMessage @errors={{this.member.errors}} @property="note" />
|
||||
<p> Maximum: <b>500</b> characters. You’ve used
|
||||
{{gh-count-down-characters this.scratchMember.note 500}}</p>
|
||||
</GhFormGroup>
|
||||
|
||||
<GhFormGroup @classNames="gh-members-subscribed-checkbox mb0">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h4 class="gh-setting-title m">Subscribed to newsletter</h4>
|
||||
<p class="gh-setting-desc">If disabled, member will <em>not</em> receive newsletter emails</p>
|
||||
</div>
|
||||
<div class="for-switch">
|
||||
<label class="switch" for="subscribed-checkbox">
|
||||
<Input @checked={{this.member.subscribed}} @type="checkbox" @id="subscribed-checkbox"
|
||||
@name="subscribed" data-test-checkbox="member-subscribed" />
|
||||
<span class="input-toggle-component"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</GhFormGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.canShowStripeInfo}}
|
||||
{{#if this.subscriptions}}
|
||||
<h4 class="gh-main-section-header small bn">Stripe info</h4>
|
||||
<div class="gh-main-section-content grey">
|
||||
<section class="gh-member-stripe-info flex flex-column flex-row-ns items-start justify-between">
|
||||
<div class="flex items-start w-50">
|
||||
<div class="flex-auto">
|
||||
<table class="gh-member-stripe-table">
|
||||
<tr class="gh-member-stripe-row">
|
||||
<td class="gh-member-stripe-label">Name</td>
|
||||
<td class="gh-member-stripe-data">
|
||||
{{#if customer.name}}
|
||||
{{customer.name}}
|
||||
{{else}}
|
||||
<span class="midgrey-d1">No name</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="gh-member-stripe-row">
|
||||
<td class="gh-member-stripe-label">Email</td>
|
||||
<td class="gh-member-stripe-data gh-member-stripe-email">
|
||||
{{#if customer.email}}
|
||||
{{customer.email}}
|
||||
{{else}}
|
||||
<span class="midgrey-d1">No email</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start w-50">
|
||||
<div class="flex-auto">
|
||||
<table class="gh-member-stripe-table">
|
||||
<tr class="gh-member-stripe-row">
|
||||
<td class="gh-member-stripe-label">Card</td>
|
||||
<td class="gh-member-stripe-data">
|
||||
<span class="midgrey-d1">No card info</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div>
|
||||
<a href="https://dashboard.stripe.com/customers/{{customer.id}}" target="_blank" rel="noopener">
|
||||
View on Stripe
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<h4 class="gh-main-section-header small bn">Products</h4>
|
||||
<div class="gh-main-section-content">
|
||||
<ol class="gh-price-list gh-list">
|
||||
<li class="gh-list-row header empty">
|
||||
<div class="gh-list-header"></div>
|
||||
<div class="gh-list-header"></div>
|
||||
</li>
|
||||
<li class="gh-list-row">
|
||||
<div class="gh-list-data gh-price-list-title">
|
||||
<h3 class="gh-price-list-name">
|
||||
Product 1
|
||||
</h3>
|
||||
<p class="ma0 pa0 f8 midgrey gh-price-list-description">
|
||||
Description
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="gh-list-data gh-price-list-actions">
|
||||
<div class="gh-price-list-actionlist">
|
||||
<LinkTo @route="settings.product">Edit</LinkTo>
|
||||
<button class="gh-btn gh-btn-link archive"><span>Cancel subscription</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="gh-list-row gh-price-list-archived">
|
||||
<div class="gh-list-data gh-price-list-title">
|
||||
<h3 class="gh-price-list-name">
|
||||
Product 2 <span class="gh-badge archived">Cancelled</span>
|
||||
</h3>
|
||||
<p class="ma0 pa0 f8 midgrey gh-price-list-description">
|
||||
Cancelled product
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="gh-list-data gh-price-list-actions">
|
||||
<div class="gh-price-list-actionlist">
|
||||
<button class="gh-btn gh-btn-link"><span>Activate</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
{{!-- <button type="button" class="gh-btn gh-btn-green" {{action (toggle "showPriceModal" this)}}>
|
||||
<span>New price</span>
|
||||
</button> --}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-main-section-block">
|
||||
<div class="gh-main-section-content bordered">
|
||||
<div class="gh-heading-xs">Emails received</div>
|
||||
<div class="gh-data-summary gh-cp-data-summary">{{@member.emailCount}}</div>
|
||||
<div class="gh-heading-xs">Emails opened</div>
|
||||
<div class="gh-data-summary gh-cp-data-summary">{{@member.emailOpenedCount}}</div>
|
||||
<div class="gh-heading-xs">Avg. open rate</div>
|
||||
<div class="gh-data-summary gh-cp-data-summary">
|
||||
{{#if (is-empty @member.emailOpenRate)}}
|
||||
<span data-tooltip="Insufficient data available">–</span>
|
||||
{{else}}
|
||||
{{@member.emailOpenRate}}%
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="gh-main-section-header small bn">Member activity</h4>
|
||||
<div class="gh-main-section-content bordered">
|
||||
<GhMemberActivityFeed @emailRecipients={{this.member.emailRecipients}} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
107
ghost/admin/app/components/gh-member-settings-form-cp.js
Normal file
107
ghost/admin/app/components/gh-member-settings-form-cp.js
Normal file
@ -0,0 +1,107 @@
|
||||
import Component from '@ember/component';
|
||||
import moment from 'moment';
|
||||
import {action} from '@ember/object';
|
||||
import {computed} from '@ember/object';
|
||||
import {gt} from '@ember/object/computed';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Component.extend({
|
||||
membersUtils: service(),
|
||||
feature: service(),
|
||||
config: service(),
|
||||
mediaQueries: service(),
|
||||
ghostPaths: service(),
|
||||
ajax: service(),
|
||||
store: service(),
|
||||
|
||||
stripeDetailsType: 'subscription',
|
||||
|
||||
// Allowed actions
|
||||
setProperty: () => {},
|
||||
|
||||
hasMultipleSubscriptions: gt('member.subscriptions', 1),
|
||||
|
||||
canShowStripeInfo: computed('member.isNew', 'membersUtils.isStripeEnabled', function () {
|
||||
let stripeEnabled = this.membersUtils.isStripeEnabled;
|
||||
|
||||
if (this.member.get('isNew') || !stripeEnabled) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
|
||||
subscriptions: computed('member.subscriptions', function () {
|
||||
let subscriptions = this.member.get('subscriptions');
|
||||
if (subscriptions && subscriptions.length > 0) {
|
||||
return subscriptions.map((subscription) => {
|
||||
const statusLabel = subscription.status ? subscription.status.replace('_', ' ') : '';
|
||||
return {
|
||||
id: subscription.id,
|
||||
customer: subscription.customer,
|
||||
name: subscription.name || '',
|
||||
email: subscription.email || '',
|
||||
status: subscription.status,
|
||||
statusLabel: statusLabel,
|
||||
startDate: subscription.start_date ? moment(subscription.start_date).format('D MMM YYYY') : '-',
|
||||
plan: subscription.plan,
|
||||
amount: parseInt(subscription.plan.amount) ? (subscription.plan.amount / 100) : 0,
|
||||
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||
cancellationReason: subscription.cancellation_reason,
|
||||
validUntil: subscription.current_period_end ? moment(subscription.current_period_end).format('D MMM YYYY') : '-'
|
||||
};
|
||||
}).reverse();
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
||||
customer: computed('subscriptions.[]', function () {
|
||||
let customer = this.subscriptions.firstObject?.customer;
|
||||
if (customer) {
|
||||
return Object.assign({}, this.subscriptions.firstObject?.customer, {
|
||||
startDate: this.subscriptions.firstObject?.startDate
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setProperty(property, value) {
|
||||
this.setProperty(property, value);
|
||||
},
|
||||
setLabels(labels) {
|
||||
this.member.set('labels', labels);
|
||||
}
|
||||
},
|
||||
|
||||
changeStripeDetailsType: action(function (type) {
|
||||
this.set('stripeDetailsType', type);
|
||||
}),
|
||||
|
||||
cancelSubscription: task(function* (subscriptionId) {
|
||||
let url = this.get('ghostPaths.url').api('members', this.member.get('id'), 'subscriptions', subscriptionId);
|
||||
|
||||
let response = yield this.ajax.put(url, {
|
||||
data: {
|
||||
cancel_at_period_end: true
|
||||
}
|
||||
});
|
||||
|
||||
this.store.pushPayload('member', response);
|
||||
return response;
|
||||
}).drop(),
|
||||
|
||||
continueSubscription: task(function* (subscriptionId) {
|
||||
let url = this.get('ghostPaths.url').api('members', this.member.get('id'), 'subscriptions', subscriptionId);
|
||||
|
||||
let response = yield this.ajax.put(url, {
|
||||
data: {
|
||||
cancel_at_period_end: false
|
||||
}
|
||||
});
|
||||
|
||||
this.store.pushPayload('member', response);
|
||||
return response;
|
||||
}).drop()
|
||||
});
|
@ -57,11 +57,15 @@ ul.nostyle li {
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1px;
|
||||
color: var(--black);
|
||||
text-transform: uppercase;
|
||||
padding: 10px 20px;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gh-list-row.header.empty .gh-list-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.gh-list:not(.tabbed) .gh-list-header:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
padding-left: 0;
|
||||
|
@ -1155,15 +1155,14 @@
|
||||
grid-column-gap: 24px;
|
||||
}
|
||||
|
||||
|
||||
.gh-main-section-content.grey {
|
||||
background: var(--main-color-content-greybg);
|
||||
}
|
||||
|
||||
.gh-main-section-content.padding-top-s {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.gh-main-section-content + .gh-main-section-header {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.gh-main-section-block.stretch-height {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
@ -1533,4 +1533,16 @@ p.gh-members-import-errordetail:first-of-type {
|
||||
.gh-members-emailpreview-container {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Custom product member details */
|
||||
.gh-cp-member-email-name {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 24px;
|
||||
}
|
||||
|
||||
.gh-cp-data-summary:not(:last-of-type) {
|
||||
margin-bottom: 24px;
|
||||
}
|
@ -27,11 +27,19 @@
|
||||
|
||||
<div>
|
||||
<form class="member-basic-info-form">
|
||||
<GhMemberSettingsForm
|
||||
@member={{this.member}}
|
||||
@scratchMember={{this.scratchMember}}
|
||||
@setProperty={{this.setProperty}}
|
||||
@isLoading={{this.isLoading}} />
|
||||
{{#if (enable-developer-experiments)}}
|
||||
<GhMemberSettingsFormCp
|
||||
@member={{this.member}}
|
||||
@scratchMember={{this.scratchMember}}
|
||||
@setProperty={{this.setProperty}}
|
||||
@isLoading={{this.isLoading}} />
|
||||
{{else}}
|
||||
<GhMemberSettingsForm
|
||||
@member={{this.member}}
|
||||
@scratchMember={{this.scratchMember}}
|
||||
@setProperty={{this.setProperty}}
|
||||
@isLoading={{this.isLoading}} />
|
||||
{{/if}}
|
||||
</form>
|
||||
|
||||
{{#unless this.member.isNew}}
|
||||
|
Loading…
Reference in New Issue
Block a user