mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
Source attribution design (#15431)
- Updated subscription box design - Added Source attribution widget to the dashboard Co-authored-by: Rishabh <zrishabhgarg@gmail.com>
This commit is contained in:
parent
66f11d5e45
commit
99f119a1f4
@ -1,41 +1,58 @@
|
||||
<section class="gh-dashboard-section gh-dashboard-recents">
|
||||
|
||||
<article class="gh-dashboard-box" {{did-insert this.loadData}}>
|
||||
<div class="gh-dashboard-recents-posts gh-dashboard-list">
|
||||
<div class="gh-dashboard-list-header">
|
||||
<div class="gh-dashboard-list-title">Sources</div>
|
||||
<div class="gh-dashboard-list-title">Free Signups</div>
|
||||
<div class="gh-dashboard-list-title">Paid Conversions</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-list-body">
|
||||
{{#each this.sources as |sourceData|}}
|
||||
<div class="gh-dashboard-list-item">
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-list-text">{{sourceData.source}}</span>
|
||||
</div>
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<h3 class="gh-dashboard-metric-label">Source attribution</h3>
|
||||
<div style="display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
grid-gap: 64px;
|
||||
">
|
||||
<div class="gh-dashboard-recents-posts gh-dashboard-list">
|
||||
<div class="gh-dashboard-list-header">
|
||||
<div class="gh-dashboard-list-title">Sources</div>
|
||||
<div class="gh-dashboard-list-title">Free Signups</div>
|
||||
<div class="gh-dashboard-list-title">Paid Conversions</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-list-body">
|
||||
{{#each this.sources as |sourceData|}}
|
||||
<div class="gh-dashboard-list-item">
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-list-text">{{sourceData.source}}</span>
|
||||
</div>
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-metric-minivalue">
|
||||
{{#if sourceData.freeSignups}}
|
||||
{{format-number sourceData.freeSignups}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-metric-minivalue">
|
||||
{{#if sourceData.freeSignups}}
|
||||
{{format-number sourceData.freeSignups}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</span>
|
||||
{{#if sourceData.paidConversions}}
|
||||
{{format-number sourceData.paidConversions}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-metric-minivalue">
|
||||
{{#if sourceData.paidConversions}}
|
||||
{{format-number sourceData.paidConversions}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</span>
|
||||
{{else}}
|
||||
<div class="gh-dashboard-list-empty">
|
||||
<p>No sources.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard-list-empty">
|
||||
<p>No sources.</p>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-left: 1px solid #eceef0; padding-left: 48px;display: flex;justify-content: center;align-items: center;">
|
||||
<div style="max-width: 200px;">
|
||||
<EmberChart
|
||||
@type='doughnut'
|
||||
@data={{this.chartData}}
|
||||
@options={{this.chartOptions}}
|
||||
@height={{400}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
@ -1,18 +1,78 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
// import {tracked} from '@glimmer/tracking';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
const CHART_COLORS = [
|
||||
'#853EED',
|
||||
'#CA3FED',
|
||||
'#E993CC',
|
||||
'#EE9696',
|
||||
'#FEC7C0'
|
||||
];
|
||||
export default class Recents extends Component {
|
||||
@service dashboardStats;
|
||||
@tracked chartType = 'free';
|
||||
|
||||
@action
|
||||
loadData() {
|
||||
this.dashboardStats.loadMemberAttributionStats();
|
||||
}
|
||||
|
||||
get chartOptions() {
|
||||
return {
|
||||
cutoutPercentage: 60,
|
||||
borderColor: '#555',
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top',
|
||||
align: 'start',
|
||||
labels: {
|
||||
color: 'rgb(255, 99, 132)',
|
||||
fontSize: 12,
|
||||
boxWidth: 10,
|
||||
padding: 3
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get chartData() {
|
||||
if (this.chartType === 'free') {
|
||||
const sortedByFree = [...this.sources];
|
||||
sortedByFree.sort((a, b) => {
|
||||
return b.freeSignups - a.freeSignups;
|
||||
});
|
||||
return {
|
||||
labels: sortedByFree.slice(0, 5).map(source => source.source),
|
||||
datasets: [{
|
||||
label: 'Free Signups',
|
||||
data: sortedByFree.slice(0, 5).map(source => source.freeSignups),
|
||||
backgroundColor: CHART_COLORS.slice(0, 5),
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}]
|
||||
};
|
||||
} else {
|
||||
const sortedByPaid = [...this.sources];
|
||||
sortedByPaid.sort((a, b) => {
|
||||
return b.paidPercentage - a.paidPercentage;
|
||||
});
|
||||
return {
|
||||
labels: sortedByPaid.slice(0, 5).map(source => source.source),
|
||||
datasets: [{
|
||||
label: 'Paid Conversions',
|
||||
data: sortedByPaid.slice(0, 5).map(source => source.paidConversions),
|
||||
backgroundColor: CHART_COLORS.slice(0, 5),
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get sources() {
|
||||
return this.dashboardStats?.memberSourceAttributionCounts;
|
||||
return this.dashboardStats?.memberSourceAttributionCounts || [];
|
||||
}
|
||||
|
||||
get areMembersEnabled() {
|
||||
|
@ -133,6 +133,200 @@
|
||||
|
||||
{{#each this.tiers as |tier|}}
|
||||
<div class="gh-main-section-content grey gh-member-tier-container" data-test-tier={{tier.id}}>
|
||||
{{#if (feature "sourceAttribution")}}
|
||||
<div class="gh-main-content-card gh-cp-membertier gh-cp-membertier-attribution gh-membertier-subscription {{if (gt tier.subscriptions.length 1) "multiple-subs" ""}}">
|
||||
{{#each tier.subscriptions as |sub index|}}
|
||||
<div class="gh-tier-card-header flex items-center">
|
||||
<div class="gh-tier-card-price">
|
||||
<div class="flex items-start">
|
||||
<span class="currency-symbol">{{sub.price.currencySymbol}}</span>
|
||||
<span class="amount">{{sub.price.nonDecimalAmount}}</span>
|
||||
</div>
|
||||
<div class="period">{{if (eq sub.price.interval "year") "yearly" "monthly"}}</div>
|
||||
</div>
|
||||
<div style="margin-left: 16px;">
|
||||
<h3 class="gh-membertier-name" data-test-text="tier-name" style="align-items:center !important; justify-content:flex-start !important;">
|
||||
{{tier.name}}
|
||||
{{#if (eq sub.status "canceled")}}
|
||||
<span class="gh-badge archived" data-test-text="member-subscription-status">Cancelled</span>
|
||||
{{else if sub.cancel_at_period_end}}
|
||||
<span class="gh-badge archived" data-test-text="member-subscription-status">Cancelled</span>
|
||||
{{else if sub.compExpiry}}
|
||||
<span class="gh-badge active" data-test-text="member-subscription-status">Active</span>
|
||||
{{else if sub.trialUntil}}
|
||||
<span class="gh-badge active" data-test-text="member-subscription-status">Active</span>
|
||||
{{else}}
|
||||
<span class="gh-badge active" data-test-text="member-subscription-status">Active</span>
|
||||
{{/if}}
|
||||
{{#if (gt tier.subscriptions.length 1)}}
|
||||
<span class="gh-membertier-subcount">{{tier.subscriptions.length}} subscriptions</span>
|
||||
{{/if}}
|
||||
</h3>
|
||||
<div>
|
||||
{{#if sub.trialUntil}}
|
||||
<span class="gh-cp-membertier-pricelabel">Free trial </span>
|
||||
{{else}}
|
||||
{{#if (or (eq sub.price.nickname "Monthly") (eq sub.price.nickname "Yearly"))}}
|
||||
{{else}}
|
||||
<span class="gh-cp-membertier-pricelabel">{{sub.price.nickname}}</span><span class="gh-cp-membertier-renewal"> –</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq sub.status "canceled")}}
|
||||
<span class="gh-cp-membertier-renewal">Ended {{sub.validUntil}}</span>
|
||||
{{else if sub.cancel_at_period_end}}
|
||||
<span class="gh-cp-membertier-renewal">Has access until {{sub.validUntil}}</span>
|
||||
{{else if sub.compExpiry}}
|
||||
<span class="gh-cp-membertier-renewal">Expires {{sub.compExpiry}}</span>
|
||||
{{else if sub.trialUntil}}
|
||||
<span class="gh-cp-membertier-renewal">Ends {{sub.trialUntil}}</span>
|
||||
{{else}}
|
||||
<span class="gh-cp-membertier-renewal">Renews {{sub.validUntil}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if sub.isComplimentary}}
|
||||
<span class="action-menu">
|
||||
<GhDropdownButton
|
||||
@dropdownName="subscription-menu-complimentary"
|
||||
@classNames="gh-btn gh-btn-outline gh-btn-icon fill gh-btn-subscription-action icon-only"
|
||||
@title="Actions"
|
||||
data-test-button="subscription-actions"
|
||||
>
|
||||
<span>
|
||||
{{svg-jar "dotdotdot"}}
|
||||
<span class="hidden">Subscription menu</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<GhDropdown
|
||||
@name="subscription-menu-complimentary"
|
||||
@tagName="ul"
|
||||
@classNames="tier-actions-menu dropdown-menu dropdown-align-right"
|
||||
>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
{{on "click" (fn this.removeComplimentary (or tier.id tier.tier_id))}}
|
||||
data-test-button="remove-complimentary"
|
||||
>
|
||||
<span class="red">Remove complimentary subscription</span>
|
||||
</button>
|
||||
</li>
|
||||
</GhDropdown>
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="action-menu">
|
||||
<GhDropdownButton @dropdownName="subscription-menu-{{sub.id}}" @classNames="gh-btn gh-btn-outline gh-btn-icon fill gh-btn-subscription-action icon-only" @title="Actions">
|
||||
<span>
|
||||
{{svg-jar "dotdotdot"}}
|
||||
<span class="hidden">Subscription menu</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<GhDropdown @name="subscription-menu-{{sub.id}}" @tagName="ul" @classNames="tier-actions-menu dropdown-menu dropdown-align-right">
|
||||
<li>
|
||||
<a href="https://dashboard.stripe.com/customers/{{sub.customer.id}}" target="_blank" rel="noopener noreferrer">
|
||||
View Stripe customer
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li>
|
||||
<a href="https://dashboard.stripe.com/subscriptions/{{sub.id}}" target="_blank" rel="noopener noreferrer">
|
||||
View Stripe subscription
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{#if (not-eq sub.status "canceled")}}
|
||||
{{#if sub.cancel_at_period_end}}
|
||||
<button type="button" {{on "click" (fn this.continueSubscription sub.id)}}>
|
||||
<span>Continue subscription</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="button" {{on "click" (fn this.cancelSubscription sub.id)}}>
|
||||
<span class="red">Cancel subscription</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</li>
|
||||
</GhDropdown>
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-membertier-advanced" data-test-subscription={{index}}>
|
||||
<div class="gh-membertier-details-container">
|
||||
{{#if sub.cancellationReason}}
|
||||
<div class="mb4">
|
||||
<h4>Cancellation reason</h4>
|
||||
<div class="gh-membertier-cancelreason">{{sub.cancellationReason}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if sub.offer}}
|
||||
{{#if (eq sub.offer.type "trial")}}
|
||||
<div class="mb4">
|
||||
<h4>Offer</h4>
|
||||
<span class="gh-cp-membertier-pricelabel"> {{sub.offer.name}} </span>
|
||||
– {{sub.offer.amount}} days free
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="mb4">
|
||||
<h4>Offer</h4>
|
||||
<span class="gh-cp-membertier-pricelabel"> {{sub.offer.name}} </span>
|
||||
{{#if (eq sub.offer.type 'fixed')}}
|
||||
– {{currency-symbol sub.offer.currency}}{{gh-price-amount sub.offer.amount}} off
|
||||
{{else}}
|
||||
– {{sub.offer.amount}}% off
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<div class="gh-membertier-details">
|
||||
<h4>Source</h4>
|
||||
<p>From <span class="fw6">Social: Twitter</span>
|
||||
{{#if (and sub.attribution sub.attribution.url sub.attribution.title)}}
|
||||
, subscribed on <a href="{{sub.attribution.url}}" target="_blank" rel="noopener noreferrer">{{ sub.attribution.title }}</a>
|
||||
{{/if}}
|
||||
on {{sub.startDate}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#if (eq tier.subscriptions.length 0)}}
|
||||
<div class="gh-membertier-subscription">
|
||||
<div>
|
||||
<div>
|
||||
<span class="gh-cp-membertier-pricelabel">Complimentary</span>
|
||||
<span class="gh-badge active">Active</span>
|
||||
</div>
|
||||
<div class="gh-membertier-created">Created on</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="gh-tier-card-price">
|
||||
<div class="flex items-start">
|
||||
<span class="currency-symbol">$</span>
|
||||
<span class="amount">0</span>
|
||||
</div>
|
||||
<div class="period">yearly</div>
|
||||
</div>
|
||||
<span class="action-menu">
|
||||
<GhDropdownButton @dropdownName="subscription-menu-complimentary" @classNames="gh-btn gh-btn-outline gh-btn-icon fill gh-btn-subscription-action icon-only" @title="Actions">
|
||||
<span>
|
||||
{{svg-jar "dotdotdot"}}
|
||||
<span class="hidden">Subscription menu</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<GhDropdown @name="subscription-menu-complimentary" @tagName="ul" @classNames="tier-actions-menu dropdown-menu dropdown-align-right">
|
||||
<li>
|
||||
<button type="button" {{on "click" (fn this.removeComplimentary tier.id)}}>
|
||||
<span class="red">Remove complimentary subscription</span>
|
||||
</button>
|
||||
</li>
|
||||
</GhDropdown>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-main-content-card gh-cp-membertier {{if (gt tier.subscriptions.length 1) "multiple-subs" ""}}">
|
||||
<h3 class="gh-membertier-name" data-test-text="tier-name">
|
||||
{{tier.name}}
|
||||
@ -315,6 +509,7 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
|
@ -258,7 +258,9 @@ export default class DashboardStatsService extends Service {
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}, []).sort((a, b) => {
|
||||
return (b.freeSignups + b.paidConversions) - (a.freeSignups - a.paidConversions);
|
||||
});
|
||||
}
|
||||
|
||||
get currentMRRTrend() {
|
||||
|
@ -2406,4 +2406,88 @@ p.gh-members-import-errordetail:first-of-type {
|
||||
.gh-contentfilter-menu:last-of-type {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* This needs to be moved once the flag is removed */
|
||||
.gh-cp-membertier-attribution.gh-membertier-subscription {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .gh-tier-card-header {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .tier-actions-menu {
|
||||
top: calc(100% + 6px);
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .gh-tier-card-price {
|
||||
border: none !important;
|
||||
background: #f5f6f6;
|
||||
padding: 10px !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .amount {
|
||||
font-weight: bold !important;
|
||||
font-size: 2.4rem !important;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .currency-symbol {
|
||||
font-weight: bold !important;
|
||||
font-size: 1.4rem !important;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .gh-membertier-name {
|
||||
font-weight: bold !important;
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .gh-badge {
|
||||
margin-bottom: -1px !important;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .action-menu {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution span.archived {
|
||||
background: #e4e8ec;
|
||||
color: #7c8b9a;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .gh-membertier-advanced {
|
||||
padding-top: 16px;
|
||||
border-top:1px solid #ECEEF0;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .gh-membertier-advanced h4 {
|
||||
font-size: 1.1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .03em;
|
||||
font-weight: 500;
|
||||
color: #7F8B99;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .gh-membertier-details p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .gh-membertier-details span, .gh-cp-membertier-attribution .gh-membertier-details a {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: #15171A;
|
||||
}
|
||||
|
||||
.gh-cp-membertier-attribution .gh-cp-membertier-renewal {
|
||||
color: #7c8b9a;
|
||||
}
|
Loading…
Reference in New Issue
Block a user