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:
Djordje Vlaisavljevic 2022-09-19 10:48:24 +02:00 committed by GitHub
parent 66f11d5e45
commit 99f119a1f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 394 additions and 36 deletions

View File

@ -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}}
&mdash;
{{/if}}
</span>
</div>
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-metric-minivalue">
{{#if sourceData.freeSignups}}
{{format-number sourceData.freeSignups}}
{{else}}
&mdash;
{{/if}}
</span>
{{#if sourceData.paidConversions}}
{{format-number sourceData.paidConversions}}
{{else}}
&mdash;
{{/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}}
&mdash;
{{/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>

View File

@ -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() {

View File

@ -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"> &ndash;</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>
&ndash; {{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')}}
&ndash; {{currency-symbol sub.offer.currency}}{{gh-price-amount sub.offer.amount}} off
{{else}}
&ndash; {{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}}

View File

@ -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() {

View File

@ -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;
}