Added free membership settings modal

refs https://github.com/TryGhost/Team/issues/648

All sites will include a default Free "Product" which is used for free memberships. This change adds UI for handling free membership settings. Also -

- Updates product icons in list and responsive sizes
- Copy updates
This commit is contained in:
Rishabh 2021-05-04 21:20:05 +05:30 committed by Rishabh Garg
parent 50035e4f46
commit e79f7f21ba
8 changed files with 291 additions and 124 deletions

View File

@ -150,81 +150,74 @@
{{/if}}
<h4 class="gh-main-section-header small bn">Products</h4>
<div class="gh-main-section-content">
<ol class="gh-memberproduct-list gh-list">
<li class="gh-list-row header empty">
<div class="gh-list-header"></div>
</li>
{{#unless this.products}}
<li class="gh-list-row">
<div class="gh-list-data gh-cp-memberproduct-noproduct">
<div class="mb2">This member doesn't have any products.</div>
{{#unless this.member.isNew}}
<button type="button" class="gh-btn gh-btn-text green gh-btn-icon gh-btn-addproduct" {{action (toggle "showMemberProductModal" this)}}>
<span>{{svg-jar "add"}} Add product</span>
</button>
{{/unless}}
</div>
</li>
{{/unless}}
{{#each this.products as |product|}}
<li class="gh-list-row">
<div class="gh-list-data gh-cp-memberproduct-title">
<h3 class="gh-memberproduct-name">
{{product.name}}
</h3>
{{#each product.subscriptions as |sub|}}
<div class="gh-memberproduct-description gh-cp-memberproduct-showmore">
<input type="checkbox" id="show-more-{{sub.id}}" role="button">
<label for="show-more-{{sub.id}}">
{{svg-jar "arrow-right"}}
<span class="gh-cp-memberproduct-pricelabel">{{sub.price.nickname}}</span>
&ndash;
<span class="gh-cp-memberproduct-price">{{sub.price.currencySymbol}}{{sub.price.nonDecimalAmount}}/{{sub.price.interval}}</span>
&ndash;
{{#if sub.cancel_at_period_end}}
<span class="gh-cp-memberproduct-renewal">Cancels {{sub.validUntil}}</span>
<span class="gh-badge archived">Cancelled</span>
{{else}}
<span class="gh-cp-memberproduct-renewal">Renews {{sub.validUntil}}</span>
<span class="gh-badge active">Active</span>
{{/if}}
</label>
<div class="details">
<span>Created on {{sub.startDate}}</span>
<div class="actions">
<a href="https://dashboard.stripe.com/subscriptions/{{sub.id}}" target="_blank" rel="noopener">
View on Stripe
</a>
<span class="ml1 mr1 dib">&ndash;</span>
{{#if sub.cancel_at_period_end}}
<button class="gh-btn gh-btn-link gh-btn-text green" {{action "continueSubscription" sub.id}}>
<span>Continue subscription</span>
</button>
{{else}}
<button class="gh-btn gh-btn-link gh-btn-text red" {{action "cancelSubscription" sub.id}}>
<span>Cancel subscription</span>
</button>
{{/if}}
</div>
</div>
</div>
{{/each}}
</div>
</li>
{{/each}}
{{#if this.products}}
<tr class="gh-list-row gh-memberproduct-add-product">
<td colspan="2" class="gh-list-data">
<button type="button" class="gh-btn gh-btn-text green gh-btn-icon gh-btn-addproduct" {{action (toggle "showMemberProductModal" this)}}>
<span>{{svg-jar "add"}} Add product</span>
</button>
</td>
</tr>
{{/if}}
</ol>
{{#unless this.products}}
<div class="gh-main-section-content bordered">
<div class="gh-cp-memberproduct-noproduct">
<div class="mb2">This member doesn't have any products.</div>
{{#unless this.member.isNew}}
<button type="button" class="gh-btn gh-btn-text green gh-btn-icon gh-btn-addproduct" {{action (toggle "showMemberProductModal" this)}}>
<span>{{svg-jar "add"}} Add product</span>
</button>
{{/unless}}
</div>
</div>
{{/unless}}
{{#each this.products as |product|}}
<div class="gh-main-section-content bordered">
<div class="gh-cp-memberproduct">
<h3 class="gh-memberproduct-name">
{{product.name}}
</h3>
{{#each product.subscriptions as |sub|}}
<div class="gh-memberproduct-subscription">
<div>
<span class="gh-cp-memberproduct-pricelabel">{{sub.price.nickname}}</span>
&ndash;
<span class="gh-cp-memberproduct-price">{{sub.price.currencySymbol}}{{sub.price.nonDecimalAmount}}/{{sub.price.interval}}</span>
&ndash;
{{#if sub.cancel_at_period_end}}
<span class="gh-cp-memberproduct-renewal">Cancels {{sub.validUntil}}</span>
<span class="gh-badge archived">Cancelled</span>
{{else}}
<span class="gh-cp-memberproduct-renewal">Renews {{sub.validUntil}}</span>
<span class="gh-badge active">Active</span>
{{/if}}
</div>
<div>Created on {{sub.startDate}}</div>
<span class="action-menu">
<GhDropdownButton @dropdownName="subscription-menu-{{sub.id}}" @classNames="gh-btn gh-btn-outline gh-btn-icon only-has-icon" @title="User Actions">
<span>
{{svg-jar "dotdotdot"}}
<span class="hidden">Subscription menu</span>
</span>
</GhDropdownButton>
<GhDropdown @name="subscription-menu-{{sub.id}}" @tagName="ul" @classNames="product-actions-menu dropdown-menu dropdown-align-right">
<li>
<a href="https://dashboard.stripe.com/subscriptions/{{sub.id}}" target="_blank" rel="noopener">
View on Stripe
</a>
</li>
<li>
{{#if sub.cancel_at_period_end}}
<button class="gh-btn gh-btn-link gh-btn-text green" {{action "continueSubscription" sub.id}}>
<span>Continue subscription</span>
</button>
{{else}}
<button class="gh-btn gh-btn-link gh-btn-text red" {{action "cancelSubscription" sub.id}}>
<span>Cancel subscription</span>
</button>
{{/if}}
</li>
</GhDropdown>
</span>
</div>
{{/each}}
</div>
</div>
{{/each}}
</div>
<div class="gh-main-section-block">
<div class="gh-main-section-content bordered">

View File

@ -0,0 +1,61 @@
<header class="modal-header" data-test-modal="webhook-form">
<h1 data-test-text="title">Free membership settings</h1>
</header>
<button class="close" href title="Close" {{action "closeModal"}} {{action (optional this.noop) on="mouseDown"}}>
{{svg-jar "close"}}
</button>
<form>
<div class="modal-body">
<div class="gh-main-section-block">
<div class="gh-main-section-content grey gh-product-priceform-block">
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="name">
<label for="name" class="fw6">Portal display name</label>
<GhTextInput
@value={{readonly this.price.nickname}}
{{!-- @input={{action (mut this.price.nickname) value="target.value"}} --}}
@name="name"
@id="name"
@class="gh-input" />
<GhErrorMessage @errors={{this.price.errors}} @property="name" />
</GhFormGroup>
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="description">
<label for="description" class="fw6">Description</label>
<GhTextInput
@value=''
@name="description"
@id="description"
@class="gh-input" />
<GhErrorMessage @errors={{this.price.errors}} @property="description" />
</GhFormGroup>
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="welcome-page">
<label for="welcome-page" class="fw6">Welcome page</label>
<GhTextInput
@value=''
@name="welcome-page"
@id="welcome-page"
@class="gh-input" />
<p>Redirect to this URL after signing up for a free membership</p>
<GhErrorMessage @errors={{this.price.errors}} @property="welcome-page" />
</GhFormGroup>
</div>
</div>
</div>
</form>
<div class="modal-footer">
<button
class="gh-btn"
{{action "closeModal"}}
{{!-- disable mouseDown so it doesn't trigger focus-out validations --}}
{{!-- {{action (optional this.noop) on="mouseDown"}} --}}
data-test-button="cancel-webhook"
>
<span>Cancel</span>
</button>
<GhTaskButton @buttonText="Save"
{{!-- @successText={{this.successText}} --}}
{{!-- @task={{this.savePrice}} --}}
@class="gh-btn gh-btn-black gh-btn-icon"
data-test-button="save-price" />
</div>

View File

@ -0,0 +1,24 @@
import ModalBase from 'ghost-admin/components/modal-base';
import classic from 'ember-classic-decorator';
import {action} from '@ember/object';
// TODO: update modals to work fully with Glimmer components
@classic
export default class ModalFreeMembershipSettings extends ModalBase {
init() {
super.init(...arguments);
}
@action
close(event) {
event?.preventDefault?.();
this.closeModal();
}
actions = {
// needed because ModalBase uses .send() for keyboard events
closeModal() {
this.close();
}
}
}

View File

@ -71,18 +71,6 @@
<h4 class="gh-main-section-header small bn">Advanced</h4>
<div class="gh-main-section-content grey gh-product-priceform-block">
{{!-- <GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="trial-period">
<label for="trial-period" class="fw6">Trial period</label>
<div class="flex items-center justify-center gh-input-group gh-labs-price-label">
<GhTextInput
@value=''
@name="trial-period"
@id="trial-period"
@class="gh-input" />
<span class="gh-input-append">days</span>
</div>
<GhErrorMessage @errors={{this.price.errors}} @property="trial-period" />
</GhFormGroup> --}}
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="welcome-page">
<label for="welcome-page" class="fw6">Welcome page</label>
<GhTextInput

View File

@ -1,7 +1,35 @@
import Controller from '@ember/controller';
import {action} from '@ember/object';
import {htmlSafe} from '@ember/string';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class ProductsController extends Controller {
@service config;
@tracked iconStyle = '';
@tracked showFreeMembershipModal = false;
constructor() {
super(...arguments);
this.iconStyle = this.setIconStyle();
}
get products() {
return this.model.sortBy('name');
}
setIconStyle() {
let icon = this.config.get('icon');
if (icon) {
return htmlSafe(`background-image: url(${icon})`);
}
icon = 'https://static.ghost.org/v4.0.0/images/ghost-orb-2.png';
return htmlSafe(`background-image: url(${icon})`);
}
@action
closeFreeMembershipModal() {
this.showFreeMembershipModal = false;
}
}

View File

@ -1566,7 +1566,8 @@ p.gh-members-import-errordetail:first-of-type {
/* Member's product list */
.gh-memberproduct-name {
margin-bottom: 6px !important;
font-size: 1.5rem;
margin-bottom: 4px !important;
}
.gh-memberproduct-actionlist {
@ -1715,4 +1716,22 @@ p.gh-members-import-errordetail:first-of-type {
.gh-btn-add-memberproduct[disabled] span {
color: var(--midgrey);
}
.gh-memberproduct-subscription {
position: relative;
}
.gh-memberproduct-subscription .action-menu {
position: absolute;
top: 0;
right: 0;
}
.gh-memberproduct-subscription .action-menu .gh-btn span {
height: 28px;
}
.gh-memberproduct-subscription .action-menu .gh-btn svg {
margin: 0;
}

View File

@ -1,24 +1,69 @@
/* Product list */
.gh-product-list .gh-list-row.header .gh-list-header {
margin: 0;
padding: 0;
height: 0;
line-height: 0;
.gh-product-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 32px;
}
.gh-product-list-chevron {
padding-right: 0;
@media (max-width: 980px) {
.gh-product-list {
grid-template-columns: repeat(1, 1fr);
}
}
.gh-product-chevron {
.gh-product-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
justify-content: center;
text-align: center;
border: 1px solid var(--whitegrey);
border-radius: 2px;
padding: 8vmin 48px;
}
@media (max-width: 980px) {
.gh-product-card {
padding: 4vmin 48px;
}
}
.gh-product-card-name {
font-size: 1.75rem;
margin: 0;
}
.gh-product-card-description {
margin: 0 0 16px;
color: var(--midgrey);
}
.gh-product-chevron span {
line-height: 0;
.gh-product-list-icon {
display: flex;
align-items: flex-end;
justify-content: center;
color: var(--green);
margin-bottom: 8px;
height: 72px;
}
.gh-product-list-icon svg {
width: 60px;
height: 60px;
}
.gh-product-list-siteicon {
width: 54px;
height: 54px;
background-color: transparent;
background-size: 54px;
border-radius: 3px;
margin-bottom: 6px;
}
.gh-product-list-icon svg circle,
.gh-product-list-icon svg path {
stroke-width: 1px !important;
}
/* Product details */

View File

@ -11,32 +11,41 @@
</GhCanvasHeader>
<section class="view-container">
<section class="content-list">
<ol class="gh-product-list gh-list">
<li class="gh-list-row header">
<div class="gh-list-header gh-list-cellwidth-70"></div>
<div class="gh-list-header gh-list-cellwidth-30"></div>
</li>
{{#each this.products as |product|}}
<li class="gh-list-row">
<LinkTo @route="settings.product" @model={{product}} class="gh-list-data gh-list-cellwidth-70 gh-product-list-title">
<h3 class="gh-product-list-name">
{{product.name}}
</h3>
<p class="ma0 pa0 f8 midgrey gh-product-list-description">
Product description
</p>
</LinkTo>
<div class="gh-product-list">
<div class="gh-product-card">
<span class="gh-product-list-icon">{{svg-jar "members"}}</span>
<h3 class="gh-product-card-name">
Free membership
</h3>
<p class="gh-product-card-description">
Product description
</p>
<LinkTo @route="settings.products" class="gh-btn" {{action (toggle "showFreeMembershipModal" this)}}>
<span>Customize</span>
</LinkTo>
</div>
<LinkTo @route="settings.product" @model={{product}} class="gh-list-data gh-list-cellwidth-30 gh-product-list-chevron">
<div class="gh-product-chevron">
{{!-- <span>330 members</span> --}}
<span>{{svg-jar "arrow-right" class="w6 h6 fill-midgrey pa1"}}</span>
</div>
</LinkTo>
</li>
{{/each}}
</ol>
</section>
{{#each this.products as |product|}}
<div class="gh-product-card">
<span class="gh-product-list-icon"><div class="gh-product-list-siteicon" style={{this.iconStyle}}></div></span>
<h3 class="gh-product-card-name">
{{product.name}}
</h3>
<p class="gh-product-card-description">
Product description
</p>
<LinkTo @route="settings.product" @model={{product}} class="gh-btn">
<span>Customize</span>
</LinkTo>
</div>
{{/each}}
</div>
</section>
</section>
</section>
{{#if this.showFreeMembershipModal}}
<GhFullscreenModal
@modal="free-membership-settings"
@close={{this.closeFreeMembershipModal}}
@modifier="action wide product-ssprice" />
{{/if}}