Merge e0e0596670
into 33d033c23b
This commit is contained in:
commit
5060910450
38
Gemfile.lock
38
Gemfile.lock
|
@ -20,30 +20,31 @@ GEM
|
|||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
ast (2.4.2)
|
||||
ast (2.4.3)
|
||||
base64 (0.2.0)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.9)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.0)
|
||||
drb (2.2.1)
|
||||
connection_pool (2.5.3)
|
||||
drb (2.2.3)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.10.2)
|
||||
language_server-protocol (3.17.0.4)
|
||||
json (2.12.2)
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.6.6)
|
||||
logger (1.7.0)
|
||||
minitest (5.25.5)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.1)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.8.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
prettier_print (1.2.1)
|
||||
prism (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.12)
|
||||
rack (3.1.15)
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.10.0)
|
||||
rubocop (1.74.0)
|
||||
rubocop (1.75.7)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
|
@ -51,11 +52,12 @@ GEM
|
|||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.38.1)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-ast (1.44.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-capybara (2.22.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
|
@ -71,13 +73,13 @@ GEM
|
|||
rubocop-factory_bot (2.27.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rails (2.30.3)
|
||||
rubocop-rails (2.32.0)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.72.1, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-rspec (3.5.0)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rspec (3.6.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rspec_rails (2.31.0)
|
||||
|
@ -104,4 +106,4 @@ DEPENDENCIES
|
|||
translations-manager!
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.6
|
||||
2.6.9
|
||||
|
|
|
@ -0,0 +1,399 @@
|
|||
import Component from "@ember/component";
|
||||
import { concat } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { later } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { classNameBindings } from "@ember-decorators/component";
|
||||
import { observes } from "@ember-decorators/object";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import avatar from "discourse/helpers/avatar";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import htmlSafe from "discourse/helpers/html-safe";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { setting } from "discourse/lib/computed";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import formatCurrency from "../helpers/format-currency";
|
||||
|
||||
const SIDEBAR_BODY_CLASS = "subscription-campaign-sidebar";
|
||||
|
||||
@classNameBindings("isGoalMet:goal-met")
|
||||
export default class CampaignBanner extends Component {
|
||||
@service router;
|
||||
|
||||
dismissed = false;
|
||||
loading = false;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_banner_shadow_color")
|
||||
dropShadowColor;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_banner_bg_image")
|
||||
backgroundImageUrl;
|
||||
|
||||
@equal(
|
||||
"siteSettings.discourse_subscriptions_campaign_banner_location",
|
||||
"Sidebar"
|
||||
)
|
||||
isSidebar;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_subscribers") subscribers;
|
||||
|
||||
@equal("siteSettings.discourse_subscriptions_campaign_type", "Subscribers")
|
||||
subscriberGoal;
|
||||
|
||||
@setting("discourse_subscriptions_currency") currency;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_amount_raised") amountRaised;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_goal") goalTarget;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_product") product;
|
||||
|
||||
@setting("discourse_subscriptions_pricing_table_enabled") pricingTableEnabled;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_show_contributors")
|
||||
showContributors;
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
|
||||
this.set("contributors", []);
|
||||
|
||||
// add background-image url to stylesheet
|
||||
if (this.backgroundImageUrl) {
|
||||
const backgroundUrl = `url(${this.backgroundImageUrl}`.replace(/\\/g, "");
|
||||
if (
|
||||
document.documentElement.style.getPropertyValue(
|
||||
"--campaign-background-image"
|
||||
) !== backgroundUrl
|
||||
) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--campaign-background-image",
|
||||
backgroundUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentUser && this.showContributors) {
|
||||
return ajax("/s/contributors", { method: "get" }).then((result) => {
|
||||
this.setProperties({
|
||||
contributors: result,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
didInsertElement() {
|
||||
super.didInsertElement(...arguments);
|
||||
if (this.isSidebar && this.shouldShow && !this.site.mobileView) {
|
||||
document.body.classList.add(SIDEBAR_BODY_CLASS);
|
||||
} else {
|
||||
document.body.classList.remove(SIDEBAR_BODY_CLASS);
|
||||
}
|
||||
|
||||
// makes sure to only play animation once, & not repeat on reload
|
||||
if (this.isGoalMet) {
|
||||
const successAnimationKey = this.keyValueStore.get(
|
||||
"campaign_success_animation"
|
||||
);
|
||||
|
||||
if (!successAnimationKey) {
|
||||
later(() => {
|
||||
this.keyValueStore.set({
|
||||
key: "campaign_success_animation",
|
||||
value: Date.now(),
|
||||
});
|
||||
document.body.classList.add("success-animation-off");
|
||||
}, 7000);
|
||||
} else {
|
||||
document.body.classList.add("success-animation-off");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
willDestroyElement() {
|
||||
super.willDestroyElement(...arguments);
|
||||
document.body.classList.remove(SIDEBAR_BODY_CLASS);
|
||||
}
|
||||
|
||||
@discourseComputed("backgroundImageUrl")
|
||||
bannerInfoStyle(backgroundImageUrl) {
|
||||
if (!backgroundImageUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `background-image: linear-gradient(
|
||||
0deg,
|
||||
rgba(var(--secondary-rgb), 0.75) 0%,
|
||||
rgba(var(--secondary-rgb), 0.75) 100%),
|
||||
var(--campaign-background-image);
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;`;
|
||||
}
|
||||
|
||||
@discourseComputed(
|
||||
"router.currentRouteName",
|
||||
"currentUser",
|
||||
"siteSettings.discourse_subscriptions_campaign_enabled",
|
||||
"visible"
|
||||
)
|
||||
shouldShow(currentRoute, currentUser, enabled, visible) {
|
||||
// do not show on admin or subscriptions pages
|
||||
const showOnRoute =
|
||||
currentRoute !== "discovery.s" &&
|
||||
!currentRoute.split(".")[0].includes("admin") &&
|
||||
currentRoute.split(".")[0] !== "subscribe" &&
|
||||
currentRoute.split(".")[0] !== "subscriptions";
|
||||
|
||||
if (!this.site.show_campaign_banner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// make sure not to render above main container when inside a topic
|
||||
if (
|
||||
this.connectorName === "above-main-container" &&
|
||||
currentRoute.includes("topic")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return showOnRoute && currentUser && enabled && visible;
|
||||
}
|
||||
|
||||
@observes("dismissed")
|
||||
_updateBodyClasses() {
|
||||
if (this.dismissed) {
|
||||
document.body.classList.remove(SIDEBAR_BODY_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
@discourseComputed("dismissed")
|
||||
visible(dismissed) {
|
||||
const dismissedBannerKey = this.keyValueStore.get(
|
||||
"dismissed_campaign_banner"
|
||||
);
|
||||
const threeMonths = 2628000000 * 3;
|
||||
|
||||
const bannerDismissedTime = new Date(dismissedBannerKey);
|
||||
const now = Date.now();
|
||||
|
||||
return (
|
||||
(!dismissedBannerKey || now - bannerDismissedTime > threeMonths) &&
|
||||
!dismissed
|
||||
);
|
||||
}
|
||||
|
||||
@discourseComputed
|
||||
subscribeRoute() {
|
||||
if (this.pricingTableEnabled) {
|
||||
return "subscriptions";
|
||||
}
|
||||
return "subscribe";
|
||||
}
|
||||
|
||||
@discourseComputed
|
||||
isGoalMet() {
|
||||
const currentVolume = this.subscriberGoal
|
||||
? this.subscribers
|
||||
: this.amountRaised;
|
||||
return currentVolume >= this.goalTarget;
|
||||
}
|
||||
|
||||
@action
|
||||
dismissBanner() {
|
||||
this.set("dismissed", true);
|
||||
this.keyValueStore.set({
|
||||
key: "dismissed_campaign_banner",
|
||||
value: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.shouldShow}}
|
||||
<div
|
||||
class="campaign-banner"
|
||||
style={{htmlSafe (concat "box-shadow: 5px 5px #" this.dropShadowColor)}}
|
||||
>
|
||||
<DButton @icon="xmark" @action={{this.dismissBanner}} class="close" />
|
||||
|
||||
<div
|
||||
class="campaign-banner-info"
|
||||
style={{htmlSafe this.bannerInfoStyle}}
|
||||
>
|
||||
{{#if this.isGoalMet}}
|
||||
<h2 class="campaign-banner-info-header">
|
||||
{{i18n "discourse_subscriptions.campaign.success_title"}}
|
||||
</h2>
|
||||
|
||||
<p class="campaign-banner-info-description">
|
||||
{{i18n "discourse_subscriptions.campaign.success_body"}}
|
||||
</p>
|
||||
{{else}}
|
||||
<h2 class="campaign-banner-info-header">
|
||||
{{i18n "discourse_subscriptions.campaign.title"}}
|
||||
</h2>
|
||||
|
||||
<p class="campaign-banner-info-description">
|
||||
{{i18n "discourse_subscriptions.campaign.body"}}
|
||||
</p>
|
||||
|
||||
{{#if this.product}}
|
||||
<LinkTo
|
||||
@route="subscribe.show"
|
||||
@model={{this.product}}
|
||||
@disabled={{this.product.subscribed}}
|
||||
class="btn btn-primary campaign-banner-info-button"
|
||||
>
|
||||
{{icon "far-heart"}}
|
||||
{{icon "heart" class="hover-heart"}}
|
||||
{{i18n "discourse_subscriptions.campaign.button"}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo
|
||||
@route={{this.subscribeRoute}}
|
||||
class="btn btn-primary campaign-banner-info-button"
|
||||
>
|
||||
{{icon "far-heart"}}
|
||||
{{icon "heart" class="hover-heart"}}
|
||||
{{i18n "discourse_subscriptions.campaign.button"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="campaign-banner-progress">
|
||||
{{#if this.isGoalMet}}
|
||||
<div class="fireworks">
|
||||
<div class="before"></div>
|
||||
<div class="after"></div>
|
||||
</div>
|
||||
|
||||
<div class="campaign-banner-progress-success"></div>
|
||||
|
||||
{{#if this.subscriberGoal}}
|
||||
<p class="campaign-banner-progress-description">
|
||||
{{htmlSafe
|
||||
(i18n
|
||||
"discourse_subscriptions.campaign.goal_comparison"
|
||||
current=this.subscribers
|
||||
goal=this.goalTarget
|
||||
)
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.campaign.subscribers"}}
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="campaign-banner-progress-description">
|
||||
{{htmlSafe
|
||||
(i18n
|
||||
"discourse_subscriptions.campaign.goal_comparison"
|
||||
current=(formatCurrency this.currency this.amountRaised)
|
||||
goal=(formatCurrency this.currency this.goalTarget)
|
||||
)
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.campaign.raised"}}
|
||||
</p>
|
||||
|
||||
{{#if this.showContributors}}
|
||||
<ConditionalLoadingSpinner
|
||||
@condition={{this.loading}}
|
||||
@size="small"
|
||||
>
|
||||
<div class="campaign-banner-progress-users">
|
||||
<p class="campaign-banner-progress-users-title">
|
||||
<strong>
|
||||
{{i18n
|
||||
"discourse_subscriptions.campaign.recent_contributors"
|
||||
}}
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
<div class="campaign-banner-progress-users-avatars">
|
||||
{{#each this.contributors as |contributor|}}
|
||||
{{avatar
|
||||
contributor
|
||||
avatarTemplatePath="avatar_template"
|
||||
usernamePath="username"
|
||||
namePath="name"
|
||||
imageSize="small"
|
||||
}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</ConditionalLoadingSpinner>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if this.subscriberGoal}}
|
||||
<progress
|
||||
class="campaign-banner-progress-bar"
|
||||
value={{this.subscribers}}
|
||||
max={{this.siteSettings.discourse_subscriptions_campaign_goal}}
|
||||
></progress>
|
||||
|
||||
<p class="campaign-banner-progress-description">
|
||||
{{htmlSafe
|
||||
(i18n
|
||||
"discourse_subscriptions.campaign.goal_comparison"
|
||||
current=this.subscribers
|
||||
goal=this.goalTarget
|
||||
)
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.campaign.subscribers"}}
|
||||
</p>
|
||||
{{else}}
|
||||
<progress
|
||||
class="campaign-banner-progress-bar"
|
||||
value={{this.amountRaised}}
|
||||
max={{this.siteSettings.discourse_subscriptions_campaign_goal}}
|
||||
></progress>
|
||||
|
||||
<p class="campaign-banner-progress-description">
|
||||
{{htmlSafe
|
||||
(i18n
|
||||
"discourse_subscriptions.campaign.goal_comparison"
|
||||
current=(formatCurrency this.currency this.amountRaised)
|
||||
goal=(formatCurrency this.currency this.goalTarget)
|
||||
)
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.campaign.raised"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showContributors}}
|
||||
<ConditionalLoadingSpinner
|
||||
@condition={{this.loading}}
|
||||
@size="small"
|
||||
>
|
||||
<div class="campaign-banner-progress-users">
|
||||
<p class="campaign-banner-progress-users-title">
|
||||
<strong>
|
||||
{{i18n
|
||||
"discourse_subscriptions.campaign.recent_contributors"
|
||||
}}
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
<div class="campaign-banner-progress-users-avatars">
|
||||
{{#each this.contributors as |contributor|}}
|
||||
{{avatar
|
||||
contributor
|
||||
avatarTemplatePath="avatar_template"
|
||||
usernamePath="username"
|
||||
namePath="name"
|
||||
imageSize="small"
|
||||
}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</ConditionalLoadingSpinner>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
{{#if this.shouldShow}}
|
||||
<div
|
||||
class="campaign-banner"
|
||||
style={{html-safe (concat "box-shadow: 5px 5px #" this.dropShadowColor)}}
|
||||
>
|
||||
<DButton @icon="xmark" @action={{this.dismissBanner}} class="close" />
|
||||
|
||||
<div class="campaign-banner-info" style={{html-safe this.bannerInfoStyle}}>
|
||||
{{#if this.isGoalMet}}
|
||||
<h2 class="campaign-banner-info-header">
|
||||
{{i18n "discourse_subscriptions.campaign.success_title"}}
|
||||
</h2>
|
||||
|
||||
<p class="campaign-banner-info-description">
|
||||
{{i18n "discourse_subscriptions.campaign.success_body"}}
|
||||
</p>
|
||||
{{else}}
|
||||
<h2 class="campaign-banner-info-header">
|
||||
{{i18n "discourse_subscriptions.campaign.title"}}
|
||||
</h2>
|
||||
|
||||
<p class="campaign-banner-info-description">
|
||||
{{i18n "discourse_subscriptions.campaign.body"}}
|
||||
</p>
|
||||
|
||||
{{#if this.product}}
|
||||
<LinkTo
|
||||
@route="subscribe.show"
|
||||
@model={{this.product}}
|
||||
@disabled={{this.product.subscribed}}
|
||||
class="btn btn-primary campaign-banner-info-button"
|
||||
>
|
||||
{{d-icon "far-heart"}}
|
||||
{{d-icon "heart" class="hover-heart"}}
|
||||
{{i18n "discourse_subscriptions.campaign.button"}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo
|
||||
@route={{this.subscribeRoute}}
|
||||
class="btn btn-primary campaign-banner-info-button"
|
||||
>
|
||||
{{d-icon "far-heart"}}
|
||||
{{d-icon "heart" class="hover-heart"}}
|
||||
{{i18n "discourse_subscriptions.campaign.button"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="campaign-banner-progress">
|
||||
{{#if this.isGoalMet}}
|
||||
<div class="fireworks">
|
||||
<div class="before"></div>
|
||||
<div class="after"></div>
|
||||
</div>
|
||||
|
||||
<div class="campaign-banner-progress-success"></div>
|
||||
|
||||
{{#if this.subscriberGoal}}
|
||||
<p class="campaign-banner-progress-description">
|
||||
{{html-safe
|
||||
(i18n
|
||||
"discourse_subscriptions.campaign.goal_comparison"
|
||||
current=this.subscribers
|
||||
goal=this.goalTarget
|
||||
)
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.campaign.subscribers"}}
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="campaign-banner-progress-description">
|
||||
{{html-safe
|
||||
(i18n
|
||||
"discourse_subscriptions.campaign.goal_comparison"
|
||||
current=(format-currency this.currency this.amountRaised)
|
||||
goal=(format-currency this.currency this.goalTarget)
|
||||
)
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.campaign.raised"}}
|
||||
</p>
|
||||
|
||||
{{#if this.showContributors}}
|
||||
<ConditionalLoadingSpinner
|
||||
@condition={{this.loading}}
|
||||
@size="small"
|
||||
>
|
||||
<div class="campaign-banner-progress-users">
|
||||
<p class="campaign-banner-progress-users-title">
|
||||
<strong>
|
||||
{{i18n
|
||||
"discourse_subscriptions.campaign.recent_contributors"
|
||||
}}
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
<div class="campaign-banner-progress-users-avatars">
|
||||
{{#each this.contributors as |contributor|}}
|
||||
{{avatar
|
||||
contributor
|
||||
avatarTemplatePath="avatar_template"
|
||||
usernamePath="username"
|
||||
namePath="name"
|
||||
imageSize="small"
|
||||
}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</ConditionalLoadingSpinner>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if this.subscriberGoal}}
|
||||
<progress
|
||||
class="campaign-banner-progress-bar"
|
||||
value={{this.subscribers}}
|
||||
max={{this.siteSettings.discourse_subscriptions_campaign_goal}}
|
||||
></progress>
|
||||
|
||||
<p class="campaign-banner-progress-description">
|
||||
{{html-safe
|
||||
(i18n
|
||||
"discourse_subscriptions.campaign.goal_comparison"
|
||||
current=this.subscribers
|
||||
goal=this.goalTarget
|
||||
)
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.campaign.subscribers"}}
|
||||
</p>
|
||||
{{else}}
|
||||
<progress
|
||||
class="campaign-banner-progress-bar"
|
||||
value={{this.amountRaised}}
|
||||
max={{this.siteSettings.discourse_subscriptions_campaign_goal}}
|
||||
></progress>
|
||||
|
||||
<p class="campaign-banner-progress-description">
|
||||
{{html-safe
|
||||
(i18n
|
||||
"discourse_subscriptions.campaign.goal_comparison"
|
||||
current=(format-currency this.currency this.amountRaised)
|
||||
goal=(format-currency this.currency this.goalTarget)
|
||||
)
|
||||
}}
|
||||
{{i18n "discourse_subscriptions.campaign.raised"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showContributors}}
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} @size="small">
|
||||
<div class="campaign-banner-progress-users">
|
||||
<p class="campaign-banner-progress-users-title">
|
||||
<strong>
|
||||
{{i18n
|
||||
"discourse_subscriptions.campaign.recent_contributors"
|
||||
}}
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
<div class="campaign-banner-progress-users-avatars">
|
||||
{{#each this.contributors as |contributor|}}
|
||||
{{avatar
|
||||
contributor
|
||||
avatarTemplatePath="avatar_template"
|
||||
usernamePath="username"
|
||||
namePath="name"
|
||||
imageSize="small"
|
||||
}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</ConditionalLoadingSpinner>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,205 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import { later } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { classNameBindings } from "@ember-decorators/component";
|
||||
import { observes } from "@ember-decorators/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { setting } from "discourse/lib/computed";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
|
||||
const SIDEBAR_BODY_CLASS = "subscription-campaign-sidebar";
|
||||
|
||||
@classNameBindings("isGoalMet:goal-met")
|
||||
export default class CampaignBanner extends Component {
|
||||
@service router;
|
||||
|
||||
dismissed = false;
|
||||
loading = false;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_banner_shadow_color")
|
||||
dropShadowColor;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_banner_bg_image")
|
||||
backgroundImageUrl;
|
||||
|
||||
@equal(
|
||||
"siteSettings.discourse_subscriptions_campaign_banner_location",
|
||||
"Sidebar"
|
||||
)
|
||||
isSidebar;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_subscribers") subscribers;
|
||||
|
||||
@equal("siteSettings.discourse_subscriptions_campaign_type", "Subscribers")
|
||||
subscriberGoal;
|
||||
|
||||
@setting("discourse_subscriptions_currency") currency;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_amount_raised") amountRaised;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_goal") goalTarget;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_product") product;
|
||||
|
||||
@setting("discourse_subscriptions_pricing_table_enabled") pricingTableEnabled;
|
||||
|
||||
@setting("discourse_subscriptions_campaign_show_contributors")
|
||||
showContributors;
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
|
||||
this.set("contributors", []);
|
||||
|
||||
// add background-image url to stylesheet
|
||||
if (this.backgroundImageUrl) {
|
||||
const backgroundUrl = `url(${this.backgroundImageUrl}`.replace(/\\/g, "");
|
||||
if (
|
||||
document.documentElement.style.getPropertyValue(
|
||||
"--campaign-background-image"
|
||||
) !== backgroundUrl
|
||||
) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--campaign-background-image",
|
||||
backgroundUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentUser && this.showContributors) {
|
||||
return ajax("/s/contributors", { method: "get" }).then((result) => {
|
||||
this.setProperties({
|
||||
contributors: result,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
didInsertElement() {
|
||||
super.didInsertElement(...arguments);
|
||||
if (this.isSidebar && this.shouldShow && !this.site.mobileView) {
|
||||
document.body.classList.add(SIDEBAR_BODY_CLASS);
|
||||
} else {
|
||||
document.body.classList.remove(SIDEBAR_BODY_CLASS);
|
||||
}
|
||||
|
||||
// makes sure to only play animation once, & not repeat on reload
|
||||
if (this.isGoalMet) {
|
||||
const successAnimationKey = this.keyValueStore.get(
|
||||
"campaign_success_animation"
|
||||
);
|
||||
|
||||
if (!successAnimationKey) {
|
||||
later(() => {
|
||||
this.keyValueStore.set({
|
||||
key: "campaign_success_animation",
|
||||
value: Date.now(),
|
||||
});
|
||||
document.body.classList.add("success-animation-off");
|
||||
}, 7000);
|
||||
} else {
|
||||
document.body.classList.add("success-animation-off");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
willDestroyElement() {
|
||||
super.willDestroyElement(...arguments);
|
||||
document.body.classList.remove(SIDEBAR_BODY_CLASS);
|
||||
}
|
||||
|
||||
@discourseComputed("backgroundImageUrl")
|
||||
bannerInfoStyle(backgroundImageUrl) {
|
||||
if (!backgroundImageUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `background-image: linear-gradient(
|
||||
0deg,
|
||||
rgba(var(--secondary-rgb), 0.75) 0%,
|
||||
rgba(var(--secondary-rgb), 0.75) 100%),
|
||||
var(--campaign-background-image);
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;`;
|
||||
}
|
||||
|
||||
@discourseComputed(
|
||||
"router.currentRouteName",
|
||||
"currentUser",
|
||||
"siteSettings.discourse_subscriptions_campaign_enabled",
|
||||
"visible"
|
||||
)
|
||||
shouldShow(currentRoute, currentUser, enabled, visible) {
|
||||
// do not show on admin or subscriptions pages
|
||||
const showOnRoute =
|
||||
currentRoute !== "discovery.s" &&
|
||||
!currentRoute.split(".")[0].includes("admin") &&
|
||||
currentRoute.split(".")[0] !== "subscribe" &&
|
||||
currentRoute.split(".")[0] !== "subscriptions";
|
||||
|
||||
if (!this.site.show_campaign_banner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// make sure not to render above main container when inside a topic
|
||||
if (
|
||||
this.connectorName === "above-main-container" &&
|
||||
currentRoute.includes("topic")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return showOnRoute && currentUser && enabled && visible;
|
||||
}
|
||||
|
||||
@observes("dismissed")
|
||||
_updateBodyClasses() {
|
||||
if (this.dismissed) {
|
||||
document.body.classList.remove(SIDEBAR_BODY_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
@discourseComputed("dismissed")
|
||||
visible(dismissed) {
|
||||
const dismissedBannerKey = this.keyValueStore.get(
|
||||
"dismissed_campaign_banner"
|
||||
);
|
||||
const threeMonths = 2628000000 * 3;
|
||||
|
||||
const bannerDismissedTime = new Date(dismissedBannerKey);
|
||||
const now = Date.now();
|
||||
|
||||
return (
|
||||
(!dismissedBannerKey || now - bannerDismissedTime > threeMonths) &&
|
||||
!dismissed
|
||||
);
|
||||
}
|
||||
|
||||
@discourseComputed
|
||||
subscribeRoute() {
|
||||
if (this.pricingTableEnabled) {
|
||||
return "subscriptions";
|
||||
}
|
||||
return "subscribe";
|
||||
}
|
||||
|
||||
@discourseComputed
|
||||
isGoalMet() {
|
||||
const currentVolume = this.subscriberGoal
|
||||
? this.subscribers
|
||||
: this.amountRaised;
|
||||
return currentVolume >= this.goalTarget;
|
||||
}
|
||||
|
||||
@action
|
||||
dismissBanner() {
|
||||
this.set("dismissed", true);
|
||||
this.keyValueStore.set({
|
||||
key: "dismissed_campaign_banner",
|
||||
value: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import Component, { Input } from "@ember/component";
|
||||
import { fn } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
|
||||
export default class CreateCouponForm extends Component {
|
||||
discountType = "amount";
|
||||
discount = null;
|
||||
promoCode = null;
|
||||
active = false;
|
||||
|
||||
@discourseComputed
|
||||
discountTypes() {
|
||||
return [
|
||||
{ id: "amount", name: "Amount" },
|
||||
{ id: "percent", name: "Percent" },
|
||||
];
|
||||
}
|
||||
|
||||
@action
|
||||
createNewCoupon() {
|
||||
const createParams = {
|
||||
promo: this.promoCode,
|
||||
discount_type: this.discountType,
|
||||
discount: this.discount,
|
||||
active: this.active,
|
||||
};
|
||||
|
||||
this.create(createParams);
|
||||
}
|
||||
|
||||
@action
|
||||
cancelCreate() {
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="create-coupon-form">
|
||||
<form class="form-horizontal">
|
||||
<p>
|
||||
<label for="promo_code">
|
||||
{{i18n "discourse_subscriptions.admin.coupons.promo_code"}}
|
||||
</label>
|
||||
<Input @type="text" name="promo_code" @value={{this.promoCode}} />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="amount">
|
||||
{{i18n "discourse_subscriptions.admin.coupons.discount"}}
|
||||
</label>
|
||||
<ComboBox
|
||||
@content={{this.discountTypes}}
|
||||
@value={{this.discountType}}
|
||||
@onChange={{fn (mut this.discountType)}}
|
||||
/>
|
||||
<Input
|
||||
class="discount-amount"
|
||||
@type="text"
|
||||
name="amount"
|
||||
@value={{this.discount}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="active">
|
||||
{{i18n "discourse_subscriptions.admin.coupons.active"}}
|
||||
</label>
|
||||
<Input @type="checkbox" name="active" @checked={{this.active}} />
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<DButton
|
||||
@action={{this.createNewCoupon}}
|
||||
@label="discourse_subscriptions.admin.coupons.create"
|
||||
@title="discourse_subscriptions.admin.coupons.create"
|
||||
@icon="plus"
|
||||
class="btn-primary btn btn-icon"
|
||||
/>
|
||||
|
||||
<DButton
|
||||
@action={{this.cancelCreate}}
|
||||
label="cancel"
|
||||
@title="cancel"
|
||||
@icon="xmark"
|
||||
class="btn btn-icon"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<div class="create-coupon-form">
|
||||
<form class="form-horizontal">
|
||||
<p>
|
||||
<label for="promo_code">
|
||||
{{i18n "discourse_subscriptions.admin.coupons.promo_code"}}
|
||||
</label>
|
||||
<Input @type="text" name="promo_code" @value={{this.promoCode}} />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="amount">
|
||||
{{i18n "discourse_subscriptions.admin.coupons.discount"}}
|
||||
</label>
|
||||
<ComboBox
|
||||
@content={{this.discountTypes}}
|
||||
@value={{this.discountType}}
|
||||
@onChange={{action (mut this.discountType)}}
|
||||
/>
|
||||
<Input
|
||||
class="discount-amount"
|
||||
@type="text"
|
||||
name="amount"
|
||||
@value={{this.discount}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="active">
|
||||
{{i18n "discourse_subscriptions.admin.coupons.active"}}
|
||||
</label>
|
||||
<Input @type="checkbox" name="active" @checked={{this.active}} />
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<DButton
|
||||
@action={{action "createNewCoupon"}}
|
||||
@label="discourse_subscriptions.admin.coupons.create"
|
||||
@title="discourse_subscriptions.admin.coupons.create"
|
||||
@icon="plus"
|
||||
class="btn-primary btn btn-icon"
|
||||
/>
|
||||
|
||||
<DButton
|
||||
@action={{action "cancelCreate"}}
|
||||
label="cancel"
|
||||
@title="cancel"
|
||||
@icon="xmark"
|
||||
class="btn btn-icon"
|
||||
/>
|
||||
</div>
|
|
@ -1,35 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
|
||||
export default class CreateCouponForm extends Component {
|
||||
discountType = "amount";
|
||||
discount = null;
|
||||
promoCode = null;
|
||||
active = false;
|
||||
|
||||
@discourseComputed
|
||||
discountTypes() {
|
||||
return [
|
||||
{ id: "amount", name: "Amount" },
|
||||
{ id: "percent", name: "Percent" },
|
||||
];
|
||||
}
|
||||
|
||||
@action
|
||||
createNewCoupon() {
|
||||
const createParams = {
|
||||
promo: this.promoCode,
|
||||
discount_type: this.discountType,
|
||||
discount: this.discount,
|
||||
active: this.active,
|
||||
};
|
||||
|
||||
this.create(createParams);
|
||||
}
|
||||
|
||||
@action
|
||||
cancelCreate() {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import DButton from "discourse/components/d-button";
|
||||
import routeAction from "discourse/helpers/route-action";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
const LoginRequired = <template>
|
||||
<h3>{{i18n "discourse_subscriptions.subscribe.unauthenticated"}}</h3>
|
||||
|
||||
<DButton
|
||||
@label="log_in"
|
||||
@action={{routeAction "showLogin"}}
|
||||
@icon="user"
|
||||
class="btn btn-primary login-required subscriptions"
|
||||
/>
|
||||
</template>;
|
||||
|
||||
export default LoginRequired;
|
|
@ -1,8 +0,0 @@
|
|||
<h3>{{i18n "discourse_subscriptions.subscribe.unauthenticated"}}</h3>
|
||||
|
||||
<DButton
|
||||
@label="log_in"
|
||||
@action={{route-action "showLogin"}}
|
||||
@icon="user"
|
||||
class="btn btn-primary login-required subscriptions"
|
||||
/>
|
|
@ -1,6 +1,8 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import PaymentPlan from "./payment-plan";
|
||||
|
||||
export default class PaymentOptions extends Component {
|
||||
@discourseComputed("plans")
|
||||
|
@ -21,4 +23,20 @@ export default class PaymentOptions extends Component {
|
|||
clickPlan(plan) {
|
||||
this.set("selectedPlan", plan.id);
|
||||
}
|
||||
|
||||
<template>
|
||||
<p>
|
||||
{{i18n "discourse_subscriptions.plans.select"}}
|
||||
</p>
|
||||
|
||||
<div class="subscribe-buttons">
|
||||
{{#each this.orderedPlans as |plan|}}
|
||||
<PaymentPlan
|
||||
@plan={{plan}}
|
||||
@selectedPlan={{this.selectedPlan}}
|
||||
@clickPlan={{this.clickPlan}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<p>
|
||||
{{i18n "discourse_subscriptions.plans.select"}}
|
||||
</p>
|
||||
|
||||
<div class="subscribe-buttons">
|
||||
{{#each this.orderedPlans as |plan|}}
|
||||
<PaymentPlan
|
||||
@plan={{plan}}
|
||||
@selectedPlan={{this.selectedPlan}}
|
||||
@clickPlan={{action "clickPlan"}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
|
@ -0,0 +1,57 @@
|
|||
import Component from "@ember/component";
|
||||
import { concat } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { tagName } from "@ember-decorators/component";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import formatCurrency from "../helpers/format-currency";
|
||||
|
||||
const RECURRING = "recurring";
|
||||
|
||||
@tagName("")
|
||||
export default class PaymentPlan extends Component {
|
||||
@discourseComputed("selectedPlan")
|
||||
selectedClass(planId) {
|
||||
return planId === this.plan.id ? "btn-primary" : "";
|
||||
}
|
||||
|
||||
@discourseComputed("plan.type")
|
||||
recurringPlan(type) {
|
||||
return type === RECURRING;
|
||||
}
|
||||
|
||||
@action
|
||||
planClick() {
|
||||
this.clickPlan(this.plan);
|
||||
return false;
|
||||
}
|
||||
|
||||
<template>
|
||||
<DButton
|
||||
@action={{this.planClick}}
|
||||
class={{concatClass
|
||||
"btn-discourse-subscriptions-subscribe"
|
||||
this.selectedClass
|
||||
}}
|
||||
>
|
||||
<span class="interval">
|
||||
{{#if this.recurringPlan}}
|
||||
{{i18n
|
||||
(concat
|
||||
"discourse_subscriptions.plans.interval.adverb."
|
||||
this.plan.recurring.interval
|
||||
)
|
||||
}}
|
||||
{{else}}
|
||||
{{i18n "discourse_subscriptions.one_time_payment"}}
|
||||
{{/if}}
|
||||
</span>
|
||||
|
||||
<span class="amount">
|
||||
{{formatCurrency this.plan.currency this.plan.amountDollars}}
|
||||
</span>
|
||||
</DButton>
|
||||
</template>
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<DButton
|
||||
@action={{action "planClick"}}
|
||||
class={{concat-class
|
||||
"btn-discourse-subscriptions-subscribe"
|
||||
this.selectedClass
|
||||
}}
|
||||
>
|
||||
<span class="interval">
|
||||
{{#if this.recurringPlan}}
|
||||
{{i18n
|
||||
(concat
|
||||
"discourse_subscriptions.plans.interval.adverb."
|
||||
this.plan.recurring.interval
|
||||
)
|
||||
}}
|
||||
{{else}}
|
||||
{{i18n "discourse_subscriptions.one_time_payment"}}
|
||||
{{/if}}
|
||||
</span>
|
||||
|
||||
<span class="amount">
|
||||
{{format-currency this.plan.currency this.plan.amountDollars}}
|
||||
</span>
|
||||
</DButton>
|
|
@ -1,25 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { tagName } from "@ember-decorators/component";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
|
||||
const RECURRING = "recurring";
|
||||
|
||||
@tagName("")
|
||||
export default class PaymentPlan extends Component {
|
||||
@discourseComputed("selectedPlan")
|
||||
selectedClass(planId) {
|
||||
return planId === this.plan.id ? "btn-primary" : "";
|
||||
}
|
||||
|
||||
@discourseComputed("plan.type")
|
||||
recurringPlan(type) {
|
||||
return type === RECURRING;
|
||||
}
|
||||
|
||||
@action
|
||||
planClick() {
|
||||
this.clickPlan(this.plan);
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import Component from "@ember/component";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { classNames } from "@ember-decorators/component";
|
||||
import htmlSafe from "discourse/helpers/html-safe";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
@classNames("product")
|
||||
export default class ProductItem extends Component {
|
||||
<template>
|
||||
<h2>{{this.product.name}}</h2>
|
||||
|
||||
<p class="product-description">
|
||||
{{htmlSafe this.product.description}}
|
||||
</p>
|
||||
|
||||
{{#if this.isLoggedIn}}
|
||||
<div class="product-purchase">
|
||||
{{#if this.product.repurchaseable}}
|
||||
<LinkTo
|
||||
@route="subscribe.show"
|
||||
@model={{this.product.id}}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.subscribe.title"}}
|
||||
</LinkTo>
|
||||
|
||||
{{#if this.product.subscribed}}
|
||||
<LinkTo
|
||||
@route="user.billing.subscriptions"
|
||||
@model={{this.currentUser.username}}
|
||||
class="billing-link"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.subscribe.view_past"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if this.product.subscribed}}
|
||||
<span class="purchased">
|
||||
✓
|
||||
{{i18n "discourse_subscriptions.subscribe.purchased"}}
|
||||
</span>
|
||||
|
||||
<LinkTo
|
||||
@route="user.billing.subscriptions"
|
||||
@model={{this.currentUser.username}}
|
||||
class="billing-link"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.subscribe.go_to_billing"}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo
|
||||
@route="subscribe.show"
|
||||
@model={{this.product.id}}
|
||||
@disabled={{this.product.subscribed}}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.subscribe.title"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
<h2>{{this.product.name}}</h2>
|
||||
|
||||
<p class="product-description">
|
||||
{{html-safe this.product.description}}
|
||||
</p>
|
||||
|
||||
{{#if this.isLoggedIn}}
|
||||
<div class="product-purchase">
|
||||
{{#if this.product.repurchaseable}}
|
||||
<LinkTo
|
||||
@route="subscribe.show"
|
||||
@model={{this.product.id}}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.subscribe.title"}}
|
||||
</LinkTo>
|
||||
|
||||
{{#if this.product.subscribed}}
|
||||
<LinkTo
|
||||
@route="user.billing.subscriptions"
|
||||
@model={{this.currentUser.username}}
|
||||
class="billing-link"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.subscribe.view_past"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if this.product.subscribed}}
|
||||
<span class="purchased">
|
||||
✓
|
||||
{{i18n "discourse_subscriptions.subscribe.purchased"}}
|
||||
</span>
|
||||
|
||||
<LinkTo
|
||||
@route="user.billing.subscriptions"
|
||||
@model={{this.currentUser.username}}
|
||||
class="billing-link"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.subscribe.go_to_billing"}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo
|
||||
@route="subscribe.show"
|
||||
@model={{this.product.id}}
|
||||
@disabled={{this.product.subscribed}}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.subscribe.title"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,5 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { classNames } from "@ember-decorators/component";
|
||||
|
||||
@classNames("product")
|
||||
export default class ProductItem extends Component {}
|
|
@ -2,6 +2,8 @@ import Component from "@ember/component";
|
|||
import { isEmpty } from "@ember/utils";
|
||||
import { classNames } from "@ember-decorators/component";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ProductItem from "./product-item";
|
||||
|
||||
@classNames("product-list")
|
||||
export default class ProductList extends Component {
|
||||
|
@ -9,4 +11,14 @@ export default class ProductList extends Component {
|
|||
emptyProducts(products) {
|
||||
return isEmpty(products);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.emptyProducts}}
|
||||
<p>{{i18n "discourse_subscriptions.subscribe.no_products"}}</p>
|
||||
{{else}}
|
||||
{{#each this.products as |product|}}
|
||||
<ProductItem @product={{product}} @isLoggedIn={{this.isLoggedIn}} />
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{{#if this.emptyProducts}}
|
||||
<p>{{i18n "discourse_subscriptions.subscribe.no_products"}}</p>
|
||||
{{else}}
|
||||
{{#each this.products as |product|}}
|
||||
<ProductItem @product={{product}} @isLoggedIn={{this.isLoggedIn}} />
|
||||
{{/each}}
|
||||
{{/if}}
|
|
@ -28,4 +28,8 @@ export default class SubscribeCard extends Component {
|
|||
didDestroyElement() {
|
||||
super.didDestroyElement(...arguments);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div id="card-element"></div>
|
||||
</template>
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<div id="card-element"></div>
|
|
@ -0,0 +1,19 @@
|
|||
import Component from "@ember/component";
|
||||
import { classNames, tagName } from "@ember-decorators/component";
|
||||
import CampaignBanner from "../../components/campaign-banner";
|
||||
|
||||
@tagName("div")
|
||||
@classNames("above-main-container-outlet", "subscriptions-campaign")
|
||||
export default class SubscriptionsCampaign extends Component {
|
||||
static shouldRender(args, context) {
|
||||
const { siteSettings } = context;
|
||||
const mobileView = context.site.mobileView;
|
||||
const bannerLocation =
|
||||
siteSettings.discourse_subscriptions_campaign_banner_location;
|
||||
return (
|
||||
bannerLocation === "Top" || (bannerLocation === "Sidebar" && mobileView)
|
||||
);
|
||||
}
|
||||
|
||||
<template><CampaignBanner @connectorName="above-main-container" /></template>
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<CampaignBanner @connectorName="above-main-container" />
|
|
@ -1,12 +0,0 @@
|
|||
export default {
|
||||
shouldRender(args, component) {
|
||||
const { siteSettings } = component;
|
||||
const mobileView = component.site.mobileView;
|
||||
const bannerLocation =
|
||||
siteSettings.discourse_subscriptions_campaign_banner_location;
|
||||
|
||||
return (
|
||||
bannerLocation === "Top" || (bannerLocation === "Sidebar" && mobileView)
|
||||
);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
import Component from "@ember/component";
|
||||
import { classNames, tagName } from "@ember-decorators/component";
|
||||
import CampaignBanner from "../../components/campaign-banner";
|
||||
|
||||
@tagName("span")
|
||||
@classNames(
|
||||
"after-topic-footer-buttons-outlet",
|
||||
"subscriptions-campaign-topic-footer"
|
||||
)
|
||||
export default class SubscriptionsCampaignTopicFooter extends Component {
|
||||
static shouldRender(args, context) {
|
||||
const { siteSettings } = context;
|
||||
const bannerLocation =
|
||||
siteSettings.discourse_subscriptions_campaign_banner_location;
|
||||
return bannerLocation === "Top" || bannerLocation === "Sidebar";
|
||||
}
|
||||
|
||||
<template>
|
||||
<CampaignBanner @connectorName="after-topic-footer-buttons" />
|
||||
</template>
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<CampaignBanner @connectorName="after-topic-footer-buttons" />
|
|
@ -1,9 +0,0 @@
|
|||
export default {
|
||||
shouldRender(args, component) {
|
||||
const { siteSettings } = component;
|
||||
const bannerLocation =
|
||||
siteSettings.discourse_subscriptions_campaign_banner_location;
|
||||
|
||||
return bannerLocation === "Top" || bannerLocation === "Sidebar";
|
||||
},
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
import Component from "@ember/component";
|
||||
import { classNames, tagName } from "@ember-decorators/component";
|
||||
import CampaignBanner from "../../components/campaign-banner";
|
||||
|
||||
@tagName("div")
|
||||
@classNames("before-topic-list-outlet", "subscriptions-campaign-sidebar")
|
||||
export default class SubscriptionsCampaignSidebar extends Component {
|
||||
static shouldRender(args, context) {
|
||||
const { siteSettings } = context;
|
||||
const mobileView = context.site.mobileView;
|
||||
const bannerLocation =
|
||||
siteSettings.discourse_subscriptions_campaign_banner_location;
|
||||
return bannerLocation === "Sidebar" && !mobileView;
|
||||
}
|
||||
|
||||
<template><CampaignBanner @connectorName="before-topic-list" /></template>
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<CampaignBanner @connectorName="before-topic-list" />
|
|
@ -1,10 +0,0 @@
|
|||
export default {
|
||||
shouldRender(args, component) {
|
||||
const { siteSettings } = component;
|
||||
const mobileView = component.site.mobileView;
|
||||
const bannerLocation =
|
||||
siteSettings.discourse_subscriptions_campaign_banner_location;
|
||||
|
||||
return bannerLocation === "Sidebar" && !mobileView;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import Component from "@ember/component";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { classNames, tagName } from "@ember-decorators/component";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import userViewingSelf from "../../helpers/user-viewing-self";
|
||||
|
||||
@tagName("li")
|
||||
@classNames("user-main-nav-outlet", "billing")
|
||||
export default class Billing extends Component {
|
||||
<template>
|
||||
{{#if (userViewingSelf this.model)}}
|
||||
<LinkTo @route="user.billing">
|
||||
{{icon "far-credit-card"}}
|
||||
{{i18n "discourse_subscriptions.navigation.billing"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{{#if (user-viewing-self this.model)}}
|
||||
<LinkTo @route="user.billing">
|
||||
{{d-icon "far-credit-card"}}
|
||||
{{i18n "discourse_subscriptions.navigation.billing"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
|
@ -0,0 +1,74 @@
|
|||
import { Input } from "@ember/component";
|
||||
import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import CreateCouponForm from "../../components/create-coupon-form";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
{{#if @controller.model.unconfigured}}
|
||||
<p>{{i18n "discourse_subscriptions.admin.unconfigured"}}</p>
|
||||
<p>
|
||||
<a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">
|
||||
{{i18n "discourse_subscriptions.admin.on_meta"}}
|
||||
</a>
|
||||
</p>
|
||||
{{else}}
|
||||
{{#if @controller.model}}
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<th>{{i18n "discourse_subscriptions.admin.coupons.code"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.coupons.discount"}}</th>
|
||||
<th>{{i18n
|
||||
"discourse_subscriptions.admin.coupons.times_redeemed"
|
||||
}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.coupons.active"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.coupons.actions"}}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @controller.model as |coupon|}}
|
||||
<tr>
|
||||
<td>{{coupon.code}}</td>
|
||||
<td>{{coupon.discount}}</td>
|
||||
<td>{{coupon.times_redeemed}}</td>
|
||||
<td>
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{coupon.active}}
|
||||
{{on "click" (fn @controller.toggleActive coupon)}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<DButton
|
||||
@action={{fn @controller.deleteCoupon coupon}}
|
||||
@icon="trash-can"
|
||||
class="btn-danger btn btn-icon btn-no-text"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
|
||||
{{#unless @controller.creating}}
|
||||
<DButton
|
||||
@action={{@controller.openCreateForm}}
|
||||
@label="discourse_subscriptions.admin.coupons.create"
|
||||
@title="discourse_subscriptions.admin.coupons.create"
|
||||
@icon="plus"
|
||||
class="btn btn-icon btn-primary create-coupon"
|
||||
/>
|
||||
{{/unless}}
|
||||
|
||||
{{#if @controller.creating}}
|
||||
<CreateCouponForm
|
||||
@cancel={{@controller.closeCreateForm}}
|
||||
@create={{@controller.createNewCoupon}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</template>
|
||||
);
|
|
@ -1,61 +0,0 @@
|
|||
{{#if this.model.unconfigured}}
|
||||
<p>{{i18n "discourse_subscriptions.admin.unconfigured"}}</p>
|
||||
<p>
|
||||
<a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">
|
||||
{{i18n "discourse_subscriptions.admin.on_meta"}}
|
||||
</a>
|
||||
</p>
|
||||
{{else}}
|
||||
{{#if this.model}}
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<th>{{i18n "discourse_subscriptions.admin.coupons.code"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.coupons.discount"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.coupons.times_redeemed"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.coupons.active"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.coupons.actions"}}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |coupon|}}
|
||||
<tr>
|
||||
<td>{{coupon.code}}</td>
|
||||
<td>{{coupon.discount}}</td>
|
||||
<td>{{coupon.times_redeemed}}</td>
|
||||
<td>
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{coupon.active}}
|
||||
{{on "click" (action "toggleActive" coupon)}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<DButton
|
||||
@action={{action "deleteCoupon"}}
|
||||
@actionParam={{coupon}}
|
||||
@icon="trash-can"
|
||||
class="btn-danger btn btn-icon btn-no-text"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
|
||||
{{#unless this.creating}}
|
||||
<DButton
|
||||
@action={{action "openCreateForm"}}
|
||||
@label="discourse_subscriptions.admin.coupons.create"
|
||||
@title="discourse_subscriptions.admin.coupons.create"
|
||||
@icon="plus"
|
||||
class="btn btn-icon btn-primary create-coupon"
|
||||
/>
|
||||
{{/unless}}
|
||||
|
||||
{{#if this.creating}}
|
||||
<CreateCouponForm
|
||||
@cancel={{action "closeCreateForm"}}
|
||||
@create={{action "createNewCoupon"}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -0,0 +1,84 @@
|
|||
import { array, fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import LoadMore from "discourse/components/load-more";
|
||||
import formatDuration from "discourse/helpers/format-duration";
|
||||
import htmlSafe from "discourse/helpers/html-safe";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<h3>{{i18n "discourse_subscriptions.admin.dashboard.title"}}</h3>
|
||||
|
||||
<LoadMore
|
||||
@selector=".discourse-patrons-table tr"
|
||||
@action={{@controller.loadMore}}
|
||||
>
|
||||
{{#if @controller.model}}
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.dashboard.table.head.user"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.dashboard.table.head.payment_intent"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.dashboard.table.head.receipt_email"
|
||||
}}
|
||||
</th>
|
||||
<th
|
||||
{{on "click" (fn @controller.orderPayments "created_at")}}
|
||||
role="button"
|
||||
class="sortable"
|
||||
>
|
||||
{{i18n "created"}}
|
||||
</th>
|
||||
<th
|
||||
{{on "click" (fn @controller.orderPayments "amount")}}
|
||||
role="button"
|
||||
class="sortable amount"
|
||||
>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.dashboard.table.head.amount"
|
||||
}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @controller.model as |payment|}}
|
||||
<tr>
|
||||
<td>
|
||||
<LinkTo
|
||||
@route="adminUser.index"
|
||||
@models={{array payment.user_id payment.username}}
|
||||
>
|
||||
{{payment.username}}
|
||||
</LinkTo>
|
||||
</td>
|
||||
<td>
|
||||
<LinkTo
|
||||
@route="patrons.show"
|
||||
@model={{payment.payment_intent_id}}
|
||||
>
|
||||
{{htmlSafe payment.payment_intent_id}}
|
||||
</LinkTo>
|
||||
</td>
|
||||
<td>{{payment.receipt_email}}</td>
|
||||
<td>{{htmlSafe (formatDuration payment.created_at_age)}}</td>
|
||||
<td class="amount">{{payment.amount_currency}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
</LoadMore>
|
||||
</template>
|
||||
);
|
|
@ -1,64 +0,0 @@
|
|||
<h3>{{i18n "discourse_subscriptions.admin.dashboard.title"}}</h3>
|
||||
|
||||
<LoadMore @selector=".discourse-patrons-table tr" @action={{action "loadMore"}}>
|
||||
{{#if this.model}}
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{i18n "discourse_subscriptions.admin.dashboard.table.head.user"}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.dashboard.table.head.payment_intent"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.dashboard.table.head.receipt_email"
|
||||
}}
|
||||
</th>
|
||||
<th
|
||||
role="button"
|
||||
onclick={{action "orderPayments" "created_at"}}
|
||||
class="sortable"
|
||||
>
|
||||
{{i18n "created"}}
|
||||
</th>
|
||||
<th
|
||||
role="button"
|
||||
onclick={{action "orderPayments" "amount"}}
|
||||
class="sortable amount"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.admin.dashboard.table.head.amount"}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |payment|}}
|
||||
<tr>
|
||||
<td>
|
||||
<LinkTo
|
||||
@route="adminUser.index"
|
||||
@models={{array payment.user_id payment.username}}
|
||||
>
|
||||
{{payment.username}}
|
||||
</LinkTo>
|
||||
</td>
|
||||
<td>
|
||||
<LinkTo
|
||||
@route="patrons.show"
|
||||
@model={{payment.payment_intent_id}}
|
||||
>
|
||||
{{html-safe payment.payment_intent_id}}
|
||||
</LinkTo>
|
||||
</td>
|
||||
<td>{{payment.receipt_email}}</td>
|
||||
<td>{{html-safe (format-duration payment.created_at_age)}}</td>
|
||||
<td class="amount">{{payment.amount_currency}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
</LoadMore>
|
|
@ -0,0 +1,44 @@
|
|||
import { fn } from "@ember/helper";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import routeAction from "discourse/helpers/route-action";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.plan_id"}}</th>
|
||||
<th>{{i18n
|
||||
"discourse_subscriptions.admin.plans.plan.nickname.title"
|
||||
}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.interval"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.amount"}}</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @controller.model as |plan|}}
|
||||
<tr>
|
||||
<td>{{plan.id}}</td>
|
||||
<td>{{plan.nickname}}</td>
|
||||
<td>{{plan.interval}}</td>
|
||||
<td>{{plan.unit_amount}}</td>
|
||||
<td class="td-right">
|
||||
<DButton
|
||||
@action={{fn @controller.editPlan plan.id}}
|
||||
@icon="far-pen-to-square"
|
||||
class="btn no-text btn-icon"
|
||||
/>
|
||||
<DButton
|
||||
@action={{routeAction "destroyPlan"}}
|
||||
@actionParam={{plan}}
|
||||
@icon="trash-can"
|
||||
class="btn-danger btn no-text btn-icon"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
);
|
|
@ -1,32 +0,0 @@
|
|||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.plan_id"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.nickname.title"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.interval"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.amount"}}</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |plan|}}
|
||||
<tr>
|
||||
<td>{{plan.id}}</td>
|
||||
<td>{{plan.nickname}}</td>
|
||||
<td>{{plan.interval}}</td>
|
||||
<td>{{plan.unit_amount}}</td>
|
||||
<td class="td-right">
|
||||
<DButton
|
||||
@action={{action "editPlan" plan.id}}
|
||||
@icon="far-pen-to-square"
|
||||
class="btn no-text btn-icon"
|
||||
/>
|
||||
<DButton
|
||||
@action={{route-action "destroyPlan"}}
|
||||
@actionParam={{plan}}
|
||||
@icon="trash-can"
|
||||
class="btn-danger btn no-text btn-icon"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,90 @@
|
|||
import { LinkTo } from "@ember/routing";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import routeAction from "discourse/helpers/route-action";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import formatUnixDate from "../../helpers/format-unix-date";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
{{#if @controller.model.unconfigured}}
|
||||
<p>{{i18n "discourse_subscriptions.admin.unconfigured"}}</p>
|
||||
<p>
|
||||
<a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">
|
||||
{{i18n "discourse_subscriptions.admin.on_meta"}}
|
||||
</a>
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="btn-right">
|
||||
<LinkTo
|
||||
@route="adminPlugins.discourse-subscriptions.products.show"
|
||||
@model="new"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{{icon "plus"}}
|
||||
<span>
|
||||
{{i18n "discourse_subscriptions.admin.products.operations.new"}}
|
||||
</span>
|
||||
</LinkTo>
|
||||
</p>
|
||||
|
||||
{{#if @controller.model}}
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<th>
|
||||
{{i18n "discourse_subscriptions.admin.products.product.name"}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.products.product.created_at"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.products.product.updated_at"
|
||||
}}
|
||||
</th>
|
||||
<th class="td-right">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.active"}}
|
||||
</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each @controller.model as |product|}}
|
||||
<tr>
|
||||
<td>{{product.name}}</td>
|
||||
<td>{{formatUnixDate product.created}}</td>
|
||||
<td>{{formatUnixDate product.updated}}</td>
|
||||
<td class="td-right">{{product.active}}</td>
|
||||
<td class="td-right">
|
||||
<div class="align-buttons">
|
||||
<LinkTo
|
||||
@route="adminPlugins.discourse-subscriptions.products.show"
|
||||
@model={{product.id}}
|
||||
class="btn no-text btn-icon"
|
||||
>
|
||||
{{icon "far-pen-to-square"}}
|
||||
</LinkTo>
|
||||
|
||||
<DButton
|
||||
@action={{routeAction "destroyProduct"}}
|
||||
@actionParam={{product}}
|
||||
@icon="trash-can"
|
||||
class="btn-danger btn no-text btn-icon"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p>
|
||||
{{i18n "discourse_subscriptions.admin.products.product_help"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</template>
|
||||
);
|
|
@ -1,74 +0,0 @@
|
|||
{{#if this.model.unconfigured}}
|
||||
<p>{{i18n "discourse_subscriptions.admin.unconfigured"}}</p>
|
||||
<p>
|
||||
<a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">
|
||||
{{i18n "discourse_subscriptions.admin.on_meta"}}
|
||||
</a>
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="btn-right">
|
||||
<LinkTo
|
||||
@route="adminPlugins.discourse-subscriptions.products.show"
|
||||
@model="new"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{{d-icon "plus"}}
|
||||
<span>
|
||||
{{i18n "discourse_subscriptions.admin.products.operations.new"}}
|
||||
</span>
|
||||
</LinkTo>
|
||||
</p>
|
||||
|
||||
{{#if this.model}}
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<th>
|
||||
{{i18n "discourse_subscriptions.admin.products.product.name"}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n "discourse_subscriptions.admin.products.product.created_at"}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n "discourse_subscriptions.admin.products.product.updated_at"}}
|
||||
</th>
|
||||
<th class="td-right">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.active"}}
|
||||
</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each this.model as |product|}}
|
||||
<tr>
|
||||
<td>{{product.name}}</td>
|
||||
<td>{{format-unix-date product.created}}</td>
|
||||
<td>{{format-unix-date product.updated}}</td>
|
||||
<td class="td-right">{{product.active}}</td>
|
||||
<td class="td-right">
|
||||
<div class="align-buttons">
|
||||
<LinkTo
|
||||
@route="adminPlugins.discourse-subscriptions.products.show"
|
||||
@model={{product.id}}
|
||||
class="btn no-text btn-icon"
|
||||
>
|
||||
{{d-icon "far-pen-to-square"}}
|
||||
</LinkTo>
|
||||
|
||||
<DButton
|
||||
@action={{route-action "destroyProduct"}}
|
||||
@actionParam={{product}}
|
||||
@icon="trash-can"
|
||||
class="btn-danger btn no-text btn-icon"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p>
|
||||
{{i18n "discourse_subscriptions.admin.products.product_help"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -0,0 +1,185 @@
|
|||
import { Input } from "@ember/component";
|
||||
import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<h4>{{i18n "discourse_subscriptions.admin.plans.title"}}</h4>
|
||||
|
||||
<form class="form-horizontal">
|
||||
<p>
|
||||
<label for="product">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.name"}}
|
||||
</label>
|
||||
|
||||
<Input
|
||||
@type="text"
|
||||
name="product_name"
|
||||
@value={{@controller.model.product.name}}
|
||||
disabled={{true}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="name">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.nickname"}}
|
||||
</label>
|
||||
|
||||
<Input
|
||||
@type="text"
|
||||
name="name"
|
||||
@value={{@controller.model.plan.nickname}}
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.nickname_help"}}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="interval">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.group"}}
|
||||
</label>
|
||||
|
||||
<ComboBox
|
||||
@valueProperty="name"
|
||||
@content={{@controller.availableGroups}}
|
||||
@value={{@controller.selectedGroup}}
|
||||
@onChange={{fn (mut @controller.model.plan.metadata.group_name)}}
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.group_help"}}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="amount">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.amount"}}
|
||||
</label>
|
||||
|
||||
{{#if @controller.planFieldDisabled}}
|
||||
<Input
|
||||
class="plan-amount plan-currency"
|
||||
disabled={{true}}
|
||||
@value={{@controller.model.plan.currency}}
|
||||
/>
|
||||
{{else}}
|
||||
<ComboBox
|
||||
@disabled={{@controller.planFieldDisabled}}
|
||||
@content={{@controller.currencies}}
|
||||
@value={{@controller.model.plan.currency}}
|
||||
@onChange={{fn (mut @controller.model.plan.currency)}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<Input
|
||||
class="plan-amount"
|
||||
@type="text"
|
||||
name="name"
|
||||
@value={{@controller.model.plan.amountDollars}}
|
||||
disabled={{@controller.planFieldDisabled}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="recurring">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.recurring"}}
|
||||
</label>
|
||||
|
||||
{{#if @controller.planFieldDisabled}}
|
||||
<Input
|
||||
@type="checkbox"
|
||||
name="recurring"
|
||||
@checked={{@controller.model.plan.isRecurring}}
|
||||
disabled={{true}}
|
||||
/>
|
||||
{{else}}
|
||||
<Input
|
||||
@type="checkbox"
|
||||
name="recurring"
|
||||
@checked={{@controller.model.plan.isRecurring}}
|
||||
{{on "change" @controller.changeRecurring}}
|
||||
/>
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
{{#if @controller.model.plan.isRecurring}}
|
||||
<p>
|
||||
<label for="interval">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.interval"}}
|
||||
</label>
|
||||
|
||||
{{#if @controller.planFieldDisabled}}
|
||||
<Input disabled={{true}} @value={{@controller.selectedInterval}} />
|
||||
{{else}}
|
||||
<ComboBox
|
||||
@valueProperty="name"
|
||||
@content={{@controller.availableIntervals}}
|
||||
@value={{@controller.selectedInterval}}
|
||||
@onChange={{fn (mut @controller.selectedInterval)}}
|
||||
/>
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="trial">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.trial"}}
|
||||
({{i18n "discourse_subscriptions.optional"}})
|
||||
</label>
|
||||
|
||||
<Input
|
||||
@type="text"
|
||||
name="trial"
|
||||
@value={{@controller.model.plan.trial_period_days}}
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.trial_help"}}
|
||||
</div>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<p>
|
||||
<label for="active">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.active"}}
|
||||
</label>
|
||||
<Input
|
||||
@type="checkbox"
|
||||
name="active"
|
||||
@checked={{@controller.model.plan.active}}
|
||||
/>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<section>
|
||||
<hr />
|
||||
|
||||
<p class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.plans.operations.create_help"}}
|
||||
</p>
|
||||
|
||||
<div class="pull-right">
|
||||
{{#if @controller.model.plan.isNew}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.admin.plans.operations.create"
|
||||
@action={{@controller.createPlan}}
|
||||
@icon="plus"
|
||||
class="btn btn-primary"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.admin.plans.operations.update"
|
||||
@action={{@controller.updatePlan}}
|
||||
@icon="check"
|
||||
class="btn btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
);
|
|
@ -1,165 +0,0 @@
|
|||
<h4>{{i18n "discourse_subscriptions.admin.plans.title"}}</h4>
|
||||
|
||||
<form class="form-horizontal">
|
||||
<p>
|
||||
<label for="product">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.name"}}
|
||||
</label>
|
||||
|
||||
<Input
|
||||
@type="text"
|
||||
name="product_name"
|
||||
@value={{this.model.product.name}}
|
||||
disabled={{true}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="name">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.nickname"}}
|
||||
</label>
|
||||
|
||||
<Input @type="text" name="name" @value={{this.model.plan.nickname}} />
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.nickname_help"}}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="interval">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.group"}}
|
||||
</label>
|
||||
|
||||
<ComboBox
|
||||
@valueProperty="name"
|
||||
@content={{this.availableGroups}}
|
||||
@value={{this.selectedGroup}}
|
||||
@onChange={{action (mut this.model.plan.metadata.group_name)}}
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.group_help"}}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="amount">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.amount"}}
|
||||
</label>
|
||||
|
||||
{{#if this.planFieldDisabled}}
|
||||
<Input
|
||||
class="plan-amount plan-currency"
|
||||
disabled={{true}}
|
||||
@value={{this.model.plan.currency}}
|
||||
/>
|
||||
{{else}}
|
||||
<ComboBox
|
||||
@disabled={{this.planFieldDisabled}}
|
||||
@content={{this.currencies}}
|
||||
@value={{this.model.plan.currency}}
|
||||
@onChange={{action (mut this.model.plan.currency)}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<Input
|
||||
class="plan-amount"
|
||||
@type="text"
|
||||
name="name"
|
||||
@value={{this.model.plan.amountDollars}}
|
||||
disabled={{this.planFieldDisabled}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="recurring">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.recurring"}}
|
||||
</label>
|
||||
|
||||
{{#if this.planFieldDisabled}}
|
||||
<Input
|
||||
@type="checkbox"
|
||||
name="recurring"
|
||||
@checked={{this.model.plan.isRecurring}}
|
||||
disabled={{true}}
|
||||
/>
|
||||
{{else}}
|
||||
<Input
|
||||
@type="checkbox"
|
||||
name="recurring"
|
||||
@checked={{this.model.plan.isRecurring}}
|
||||
{{on "change" (action "changeRecurring")}}
|
||||
/>
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
{{#if this.model.plan.isRecurring}}
|
||||
<p>
|
||||
<label for="interval">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.interval"}}
|
||||
</label>
|
||||
|
||||
{{#if this.planFieldDisabled}}
|
||||
<Input disabled={{true}} @value={{this.selectedInterval}} />
|
||||
{{else}}
|
||||
<ComboBox
|
||||
@valueProperty="name"
|
||||
@content={{this.availableIntervals}}
|
||||
@value={{this.selectedInterval}}
|
||||
@onChange={{action (mut this.selectedInterval)}}
|
||||
/>
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="trial">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.trial"}}
|
||||
({{i18n "discourse_subscriptions.optional"}})
|
||||
</label>
|
||||
|
||||
<Input
|
||||
@type="text"
|
||||
name="trial"
|
||||
@value={{this.model.plan.trial_period_days}}
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.trial_help"}}
|
||||
</div>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<p>
|
||||
<label for="active">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.active"}}
|
||||
</label>
|
||||
<Input @type="checkbox" name="active" @checked={{this.model.plan.active}} />
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<section>
|
||||
<hr />
|
||||
|
||||
<p class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.plans.operations.create_help"}}
|
||||
</p>
|
||||
|
||||
<div class="pull-right">
|
||||
{{#if this.model.plan.isNew}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.admin.plans.operations.create"
|
||||
@action={{action "createPlan"}}
|
||||
@icon="plus"
|
||||
class="btn btn-primary"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.admin.plans.operations.update"
|
||||
@action={{action "updatePlan"}}
|
||||
@icon="check"
|
||||
class="btn btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
|
@ -0,0 +1,195 @@
|
|||
import { Input, Textarea } from "@ember/component";
|
||||
import { array } from "@ember/helper";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import formatCurrency from "../../helpers/format-currency";
|
||||
import formatUnixDate from "../../helpers/format-unix-date";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<h4>{{i18n "discourse_subscriptions.admin.products.title"}}</h4>
|
||||
|
||||
<form class="form-horizontal">
|
||||
<p>
|
||||
<label for="name">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.name"}}
|
||||
</label>
|
||||
<Input
|
||||
@type="text"
|
||||
name="name"
|
||||
@value={{@controller.model.product.name}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="description">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.description"}}
|
||||
</label>
|
||||
|
||||
<Textarea
|
||||
name="description"
|
||||
@value={{@controller.model.product.metadata.description}}
|
||||
class="discourse-subscriptions-admin-textarea"
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.products.product.description_help"
|
||||
}}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="statement_descriptor">
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.products.product.statement_descriptor"
|
||||
}}
|
||||
</label>
|
||||
|
||||
<Input
|
||||
@type="text"
|
||||
name="statement_descriptor"
|
||||
@value={{@controller.model.product.statement_descriptor}}
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.products.product.statement_descriptor_help"
|
||||
}}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="repurchaseable">
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.products.product.repurchaseable"
|
||||
}}
|
||||
</label>
|
||||
|
||||
<Input
|
||||
@type="checkbox"
|
||||
name="repurchaseable"
|
||||
@checked={{@controller.model.product.metadata.repurchaseable}}
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.products.product.repurchase_help"
|
||||
}}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="active">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.active"}}
|
||||
</label>
|
||||
|
||||
<Input
|
||||
@type="checkbox"
|
||||
name="active"
|
||||
@checked={{@controller.model.product.active}}
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.active_help"}}
|
||||
</div>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{{#unless @controller.model.product.isNew}}
|
||||
<h4>{{i18n "discourse_subscriptions.admin.plans.title"}}</h4>
|
||||
|
||||
<p>
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<th>{{i18n
|
||||
"discourse_subscriptions.admin.plans.plan.nickname"
|
||||
}}</th>
|
||||
<th>{{i18n
|
||||
"discourse_subscriptions.admin.plans.plan.interval"
|
||||
}}</th>
|
||||
<th>{{i18n
|
||||
"discourse_subscriptions.admin.plans.plan.created_at"
|
||||
}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.group"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.active"}}</th>
|
||||
<th class="td-right">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.amount"}}
|
||||
</th>
|
||||
<th class="td-right">
|
||||
<LinkTo
|
||||
@route="adminPlugins.discourse-subscriptions.products.show.plans.show"
|
||||
@models={{array @controller.model.product.id "new"}}
|
||||
class="btn"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.admin.plans.operations.add"}}
|
||||
</LinkTo>
|
||||
</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each @controller.model.plans as |plan|}}
|
||||
<tr>
|
||||
<td>{{plan.nickname}}</td>
|
||||
<td>{{plan.recurring.interval}}</td>
|
||||
<td>{{formatUnixDate plan.created}}</td>
|
||||
<td>{{plan.metadata.group_name}}</td>
|
||||
<td>{{plan.active}}</td>
|
||||
<td class="td-right">
|
||||
{{formatCurrency plan.currency plan.amountDollars}}
|
||||
</td>
|
||||
<td class="td-right">
|
||||
<LinkTo
|
||||
@route="adminPlugins.discourse-subscriptions.products.show.plans.show"
|
||||
@models={{array @controller.model.product.id plan.id}}
|
||||
class="btn no-text btn-icon"
|
||||
>
|
||||
{{icon "far-pen-to-square"}}
|
||||
</LinkTo>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="8">
|
||||
<hr />
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.products.product.plan_help"
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
{{/unless}}
|
||||
|
||||
<div class="pull-right">
|
||||
<DButton
|
||||
@label="cancel"
|
||||
@action={{@controller.cancelProduct}}
|
||||
@icon="xmark"
|
||||
/>
|
||||
|
||||
{{#if @controller.model.product.isNew}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.admin.products.operations.create"
|
||||
@action={{@controller.createProduct}}
|
||||
@icon="plus"
|
||||
class="btn btn-primary"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.admin.products.operations.update"
|
||||
@action={{@controller.updateProduct}}
|
||||
@icon="check"
|
||||
class="btn btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{outlet}}
|
||||
</template>
|
||||
);
|
|
@ -1,161 +0,0 @@
|
|||
<h4>{{i18n "discourse_subscriptions.admin.products.title"}}</h4>
|
||||
|
||||
<form class="form-horizontal">
|
||||
<p>
|
||||
<label for="name">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.name"}}
|
||||
</label>
|
||||
<Input @type="text" name="name" @value={{this.model.product.name}} />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="description">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.description"}}
|
||||
</label>
|
||||
|
||||
<Textarea
|
||||
name="description"
|
||||
@value={{this.model.product.metadata.description}}
|
||||
class="discourse-subscriptions-admin-textarea"
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.description_help"}}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="statement_descriptor">
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.products.product.statement_descriptor"
|
||||
}}
|
||||
</label>
|
||||
|
||||
<Input
|
||||
@type="text"
|
||||
name="statement_descriptor"
|
||||
@value={{this.model.product.statement_descriptor}}
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.products.product.statement_descriptor_help"
|
||||
}}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="repurchaseable">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.repurchaseable"}}
|
||||
</label>
|
||||
|
||||
<Input
|
||||
@type="checkbox"
|
||||
name="repurchaseable"
|
||||
@checked={{this.model.product.metadata.repurchaseable}}
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.repurchase_help"}}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="active">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.active"}}
|
||||
</label>
|
||||
|
||||
<Input
|
||||
@type="checkbox"
|
||||
name="active"
|
||||
@checked={{this.model.product.active}}
|
||||
/>
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n "discourse_subscriptions.admin.products.product.active_help"}}
|
||||
</div>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{{#unless this.model.product.isNew}}
|
||||
<h4>{{i18n "discourse_subscriptions.admin.plans.title"}}</h4>
|
||||
|
||||
<p>
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.nickname"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.interval"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.created_at"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.group"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.admin.plans.plan.active"}}</th>
|
||||
<th class="td-right">
|
||||
{{i18n "discourse_subscriptions.admin.plans.plan.amount"}}
|
||||
</th>
|
||||
<th class="td-right">
|
||||
<LinkTo
|
||||
@route="adminPlugins.discourse-subscriptions.products.show.plans.show"
|
||||
@models={{array this.model.product.id "new"}}
|
||||
class="btn"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.admin.plans.operations.add"}}
|
||||
</LinkTo>
|
||||
</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each this.model.plans as |plan|}}
|
||||
<tr>
|
||||
<td>{{plan.nickname}}</td>
|
||||
<td>{{plan.recurring.interval}}</td>
|
||||
<td>{{format-unix-date plan.created}}</td>
|
||||
<td>{{plan.metadata.group_name}}</td>
|
||||
<td>{{plan.active}}</td>
|
||||
<td class="td-right">
|
||||
{{format-currency plan.currency plan.amountDollars}}
|
||||
</td>
|
||||
<td class="td-right">
|
||||
<LinkTo
|
||||
@route="adminPlugins.discourse-subscriptions.products.show.plans.show"
|
||||
@models={{array this.model.product.id plan.id}}
|
||||
class="btn no-text btn-icon"
|
||||
>
|
||||
{{d-icon "far-pen-to-square"}}
|
||||
</LinkTo>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="8">
|
||||
<hr />
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.products.product.plan_help"
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
{{/unless}}
|
||||
|
||||
<div class="pull-right">
|
||||
<DButton @label="cancel" @action={{action "cancelProduct"}} @icon="xmark" />
|
||||
|
||||
{{#if this.model.product.isNew}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.admin.products.operations.create"
|
||||
@action={{action "createProduct"}}
|
||||
@icon="plus"
|
||||
class="btn btn-primary"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.admin.products.operations.update"
|
||||
@action={{action "updateProduct"}}
|
||||
@icon="check"
|
||||
class="btn btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{outlet}}
|
|
@ -0,0 +1,3 @@
|
|||
import RouteTemplate from "ember-route-template";
|
||||
|
||||
export default RouteTemplate(<template>{{outlet}}</template>);
|
|
@ -1 +0,0 @@
|
|||
{{outlet}}
|
|
@ -0,0 +1,105 @@
|
|||
import { fn } from "@ember/helper";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import LoadMore from "discourse/components/load-more";
|
||||
import loadingSpinner from "discourse/helpers/loading-spinner";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import formatUnixDate from "../../helpers/format-unix-date";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
{{#if @controller.model.unconfigured}}
|
||||
<p>{{i18n "discourse_subscriptions.admin.unconfigured"}}</p>
|
||||
<p>
|
||||
<a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">
|
||||
{{i18n "discourse_subscriptions.admin.on_meta"}}
|
||||
</a>
|
||||
</p>
|
||||
{{else}}
|
||||
<LoadMore
|
||||
@selector=".discourse-patrons-table tr"
|
||||
@action={{@controller.loadMore}}
|
||||
>
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.user"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.subscription_id"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.customer"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.product"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.plan"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.status"
|
||||
}}
|
||||
</th>
|
||||
<th class="td-right">
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.created_at"
|
||||
}}
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each @controller.model.data as |subscription|}}
|
||||
<tr>
|
||||
<td>
|
||||
{{#if subscription.metadataUserExists}}
|
||||
<a href={{subscription.subscriptionUserPath}}>
|
||||
{{subscription.metadata.username}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>{{subscription.id}}</td>
|
||||
<td>{{subscription.customer}}</td>
|
||||
<td>{{subscription.plan.product.name}}</td>
|
||||
<td>{{subscription.plan.nickname}}</td>
|
||||
<td>{{subscription.status}}</td>
|
||||
<td class="td-right">
|
||||
{{formatUnixDate subscription.created}}
|
||||
</td>
|
||||
<td class="td-right">
|
||||
{{#if subscription.loading}}
|
||||
{{loadingSpinner size="small"}}
|
||||
{{else}}
|
||||
<DButton
|
||||
@disabled={{subscription.canceled}}
|
||||
@label="cancel"
|
||||
@action={{fn @controller.showCancelModal subscription}}
|
||||
@icon="xmark"
|
||||
/>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</LoadMore>
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{@controller.loading}} />
|
||||
{{/if}}
|
||||
</template>
|
||||
);
|
|
@ -1,90 +0,0 @@
|
|||
{{#if this.model.unconfigured}}
|
||||
<p>{{i18n "discourse_subscriptions.admin.unconfigured"}}</p>
|
||||
<p>
|
||||
<a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">
|
||||
{{i18n "discourse_subscriptions.admin.on_meta"}}
|
||||
</a>
|
||||
</p>
|
||||
{{else}}
|
||||
<LoadMore
|
||||
@selector=".discourse-patrons-table tr"
|
||||
@action={{action "loadMore"}}
|
||||
>
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.user"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.subscription_id"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.customer"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.product"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.plan"
|
||||
}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.status"
|
||||
}}
|
||||
</th>
|
||||
<th class="td-right">
|
||||
{{i18n
|
||||
"discourse_subscriptions.admin.subscriptions.subscription.created_at"
|
||||
}}
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each this.model.data as |subscription|}}
|
||||
<tr>
|
||||
<td>
|
||||
{{#if subscription.metadataUserExists}}
|
||||
<a href={{subscription.subscriptionUserPath}}>
|
||||
{{subscription.metadata.username}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>{{subscription.id}}</td>
|
||||
<td>{{subscription.customer}}</td>
|
||||
<td>{{subscription.plan.product.name}}</td>
|
||||
<td>{{subscription.plan.nickname}}</td>
|
||||
<td>{{subscription.status}}</td>
|
||||
<td class="td-right">{{format-unix-date subscription.created}}</td>
|
||||
<td class="td-right">
|
||||
{{#if subscription.loading}}
|
||||
{{loading-spinner size="small"}}
|
||||
{{else}}
|
||||
<DButton
|
||||
@disabled={{subscription.canceled}}
|
||||
@label="cancel"
|
||||
@action={{action "showCancelModal" subscription}}
|
||||
@icon="xmark"
|
||||
/>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</LoadMore>
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||
{{/if}}
|
|
@ -0,0 +1,62 @@
|
|||
import RouteTemplate from "ember-route-template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import NavItem from "discourse/components/nav-item";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<h2>{{i18n
|
||||
"discourse_subscriptions.title"
|
||||
site_name=@controller.siteSettings.title
|
||||
}}</h2>
|
||||
|
||||
{{#if @controller.stripeConfigured}}
|
||||
<div class="discourse-subscriptions-buttons">
|
||||
{{#if @controller.campaignEnabled}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.campaign.refresh_campaign"
|
||||
@icon="rotate"
|
||||
@action={{@controller.triggerManualRefresh}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#unless @controller.campaignProductSet}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.campaign.one_click_campaign"
|
||||
@icon="square-plus"
|
||||
@action={{@controller.createOneClickCampaign}}
|
||||
@isLoading={{@controller.loading}}
|
||||
/>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-pills">
|
||||
<NavItem
|
||||
@route="adminPlugins.discourse-subscriptions.products"
|
||||
@label="discourse_subscriptions.admin.products.title"
|
||||
/>
|
||||
<NavItem
|
||||
@route="adminPlugins.discourse-subscriptions.coupons"
|
||||
@label="discourse_subscriptions.admin.coupons.title"
|
||||
/>
|
||||
<NavItem
|
||||
@route="adminPlugins.discourse-subscriptions.subscriptions"
|
||||
@label="discourse_subscriptions.admin.subscriptions.title"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<hr />
|
||||
|
||||
<div id="discourse-subscriptions-admin">
|
||||
{{outlet}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p>{{i18n "discourse_subscriptions.admin.unconfigured"}}</p>
|
||||
<p>
|
||||
<a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">
|
||||
{{i18n "discourse_subscriptions.admin.on_meta"}}
|
||||
</a>
|
||||
</p>
|
||||
{{/if}}
|
||||
</template>
|
||||
);
|
|
@ -1,53 +0,0 @@
|
|||
<h2>{{i18n
|
||||
"discourse_subscriptions.title"
|
||||
site_name=this.siteSettings.title
|
||||
}}</h2>
|
||||
|
||||
{{#if this.stripeConfigured}}
|
||||
<div class="discourse-subscriptions-buttons">
|
||||
{{#if this.campaignEnabled}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.campaign.refresh_campaign"
|
||||
@icon="rotate"
|
||||
@action={{action "triggerManualRefresh"}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#unless this.campaignProductSet}}
|
||||
<DButton
|
||||
@label="discourse_subscriptions.campaign.one_click_campaign"
|
||||
@icon="square-plus"
|
||||
@action={{action "createOneClickCampaign"}}
|
||||
@isLoading={{this.loading}}
|
||||
/>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-pills">
|
||||
<NavItem
|
||||
@route="adminPlugins.discourse-subscriptions.products"
|
||||
@label="discourse_subscriptions.admin.products.title"
|
||||
/>
|
||||
<NavItem
|
||||
@route="adminPlugins.discourse-subscriptions.coupons"
|
||||
@label="discourse_subscriptions.admin.coupons.title"
|
||||
/>
|
||||
<NavItem
|
||||
@route="adminPlugins.discourse-subscriptions.subscriptions"
|
||||
@label="discourse_subscriptions.admin.subscriptions.title"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<hr />
|
||||
|
||||
<div id="discourse-subscriptions-admin">
|
||||
{{outlet}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p>{{i18n "discourse_subscriptions.admin.unconfigured"}}</p>
|
||||
<p>
|
||||
<a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">
|
||||
{{i18n "discourse_subscriptions.admin.on_meta"}}
|
||||
</a>
|
||||
</p>
|
||||
{{/if}}
|
|
@ -0,0 +1,18 @@
|
|||
import RouteTemplate from "ember-route-template";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="title-wrapper">
|
||||
<h1>
|
||||
{{i18n "discourse_subscriptions.subscribe.title"}}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{{outlet}}
|
||||
</div>
|
||||
</template>
|
||||
);
|
|
@ -1,11 +0,0 @@
|
|||
<div class="container">
|
||||
<div class="title-wrapper">
|
||||
<h1>
|
||||
{{i18n "discourse_subscriptions.subscribe.title"}}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{{outlet}}
|
||||
</div>
|
|
@ -0,0 +1,16 @@
|
|||
import RouteTemplate from "ember-route-template";
|
||||
import LoginRequired from "../../components/login-required";
|
||||
import ProductList from "../../components/product-list";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
{{#unless @controller.isLoggedIn}}
|
||||
<LoginRequired />
|
||||
{{/unless}}
|
||||
|
||||
<ProductList
|
||||
@products={{@controller.model}}
|
||||
@isLoggedIn={{@controller.isLoggedIn}}
|
||||
/>
|
||||
</template>
|
||||
);
|
|
@ -1,5 +0,0 @@
|
|||
{{#unless this.isLoggedIn}}
|
||||
<LoginRequired />
|
||||
{{/unless}}
|
||||
|
||||
<ProductList @products={{this.model}} @isLoggedIn={{this.isLoggedIn}} />
|
|
@ -0,0 +1,151 @@
|
|||
import { Input } from "@ember/component";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import htmlSafe from "discourse/helpers/html-safe";
|
||||
import loadingSpinner from "discourse/helpers/loading-spinner";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import LoginRequired from "../../components/login-required";
|
||||
import PaymentOptions from "../../components/payment-options";
|
||||
import SubscribeCaProvinceSelect from "../../components/subscribe-ca-province-select";
|
||||
import SubscribeCard from "../../components/subscribe-card";
|
||||
import SubscribeCountrySelect from "../../components/subscribe-country-select";
|
||||
import SubscribeUsStateSelect from "../../components/subscribe-us-state-select";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<div class="discourse-subscriptions-section-columns">
|
||||
<div class="section-column discourse-subscriptions-confirmation-billing">
|
||||
<h2>
|
||||
{{@controller.model.product.name}}
|
||||
</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>
|
||||
{{htmlSafe @controller.model.product.description}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-column">
|
||||
{{#if @controller.canPurchase}}
|
||||
<h2>
|
||||
{{i18n "discourse_subscriptions.subscribe.card.title"}}
|
||||
</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
<PaymentOptions
|
||||
@plans={{@controller.model.plans}}
|
||||
@selectedPlan={{@controller.selectedPlan}}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SubscribeCard @cardElement={{@controller.cardElement}} />
|
||||
|
||||
{{#if @controller.loading}}
|
||||
{{loadingSpinner}}
|
||||
{{else if @controller.isAnonymous}}
|
||||
<LoginRequired />
|
||||
{{else}}
|
||||
<Input
|
||||
@type="text"
|
||||
name="cardholder_name"
|
||||
placeholder={{i18n
|
||||
"discourse_subscriptions.subscribe.cardholder_name"
|
||||
}}
|
||||
@value={{@controller.cardholderName}}
|
||||
class="subscribe-name"
|
||||
/>
|
||||
<div class="address-fields">
|
||||
<SubscribeCountrySelect
|
||||
@value={{@controller.cardholderAddress.country}}
|
||||
@onChange={{@controller.changeCountry}}
|
||||
/>
|
||||
<Input
|
||||
@type="text"
|
||||
name="cardholder_postal_code"
|
||||
placeholder={{i18n
|
||||
"discourse_subscriptions.subscribe.cardholder_address.postal_code"
|
||||
}}
|
||||
@value={{@controller.cardholderAddress.postalCode}}
|
||||
class="subscribe-address-postal-code"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
@type="text"
|
||||
name="cardholder_line1"
|
||||
placeholder={{i18n
|
||||
"discourse_subscriptions.subscribe.cardholder_address.line1"
|
||||
}}
|
||||
@value={{@controller.cardholderAddress.line1}}
|
||||
class="subscribe-address-line1"
|
||||
/>
|
||||
<div class="address-fields">
|
||||
<Input
|
||||
@type="text"
|
||||
name="cardholder_city"
|
||||
placeholder={{i18n
|
||||
"discourse_subscriptions.subscribe.cardholder_address.city"
|
||||
}}
|
||||
@value={{@controller.cardholderAddress.city}}
|
||||
class="subscribe-address-city"
|
||||
/>
|
||||
{{#if @controller.isCountryUS}}
|
||||
<SubscribeUsStateSelect
|
||||
@value={{@controller.cardholderAddress.state}}
|
||||
@onChange={{@controller.changeState}}
|
||||
/>
|
||||
{{else if @controller.isCountryCA}}
|
||||
<SubscribeCaProvinceSelect
|
||||
@value={{@controller.cardholderAddress.state}}
|
||||
@onChange={{@controller.changeState}}
|
||||
/>
|
||||
{{else}}
|
||||
<Input
|
||||
@type="text"
|
||||
name="cardholder_state"
|
||||
placeholder={{i18n
|
||||
"discourse_subscriptions.subscribe.cardholder_address.state"
|
||||
}}
|
||||
@value={{@controller.cardholderAddress.state}}
|
||||
class="subscribe-address-state"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
@type="text"
|
||||
name="promo_code"
|
||||
placeholder={{i18n
|
||||
"discourse_subscriptions.subscribe.promo_code"
|
||||
}}
|
||||
@value={{@controller.promoCode}}
|
||||
class="subscribe-promo-code"
|
||||
/>
|
||||
|
||||
<DButton
|
||||
@disabled={{@controller.loading}}
|
||||
@action={{@controller.stripePaymentHandler}}
|
||||
class="btn btn-primary btn-payment"
|
||||
@label="discourse_subscriptions.plans.payment_button"
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<h2>{{i18n
|
||||
"discourse_subscriptions.subscribe.already_purchased"
|
||||
}}</h2>
|
||||
|
||||
<LinkTo
|
||||
@route="user.billing.subscriptions"
|
||||
@model={{@controller.currentUser.username}}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.subscribe.go_to_billing"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
);
|
|
@ -1,129 +0,0 @@
|
|||
<div class="discourse-subscriptions-section-columns">
|
||||
<div class="section-column discourse-subscriptions-confirmation-billing">
|
||||
<h2>
|
||||
{{this.model.product.name}}
|
||||
</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>
|
||||
{{html-safe this.model.product.description}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-column">
|
||||
{{#if this.canPurchase}}
|
||||
<h2>
|
||||
{{i18n "discourse_subscriptions.subscribe.card.title"}}
|
||||
</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
<PaymentOptions
|
||||
@plans={{this.model.plans}}
|
||||
@selectedPlan={{this.selectedPlan}}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SubscribeCard @cardElement={{this.cardElement}} />
|
||||
|
||||
{{#if this.loading}}
|
||||
{{loading-spinner}}
|
||||
{{else if this.isAnonymous}}
|
||||
<LoginRequired />
|
||||
{{else}}
|
||||
<Input
|
||||
@type="text"
|
||||
name="cardholder_name"
|
||||
placeholder={{i18n
|
||||
"discourse_subscriptions.subscribe.cardholder_name"
|
||||
}}
|
||||
@value={{this.cardholderName}}
|
||||
class="subscribe-name"
|
||||
/>
|
||||
<div class="address-fields">
|
||||
<SubscribeCountrySelect
|
||||
@value={{this.cardholderAddress.country}}
|
||||
@onChange={{action "changeCountry"}}
|
||||
/>
|
||||
<Input
|
||||
@type="text"
|
||||
name="cardholder_postal_code"
|
||||
placeholder={{i18n
|
||||
"discourse_subscriptions.subscribe.cardholder_address.postal_code"
|
||||
}}
|
||||
@value={{this.cardholderAddress.postalCode}}
|
||||
class="subscribe-address-postal-code"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
@type="text"
|
||||
name="cardholder_line1"
|
||||
placeholder={{i18n
|
||||
"discourse_subscriptions.subscribe.cardholder_address.line1"
|
||||
}}
|
||||
@value={{this.cardholderAddress.line1}}
|
||||
class="subscribe-address-line1"
|
||||
/>
|
||||
<div class="address-fields">
|
||||
<Input
|
||||
@type="text"
|
||||
name="cardholder_city"
|
||||
placeholder={{i18n
|
||||
"discourse_subscriptions.subscribe.cardholder_address.city"
|
||||
}}
|
||||
@value={{this.cardholderAddress.city}}
|
||||
class="subscribe-address-city"
|
||||
/>
|
||||
{{#if this.isCountryUS}}
|
||||
<SubscribeUsStateSelect
|
||||
@value={{this.cardholderAddress.state}}
|
||||
@onChange={{action "changeState"}}
|
||||
/>
|
||||
{{else if this.isCountryCA}}
|
||||
<SubscribeCaProvinceSelect
|
||||
@value={{this.cardholderAddress.state}}
|
||||
@onChange={{action "changeState"}}
|
||||
/>
|
||||
{{else}}
|
||||
<Input
|
||||
@type="text"
|
||||
name="cardholder_state"
|
||||
placeholder={{i18n
|
||||
"discourse_subscriptions.subscribe.cardholder_address.state"
|
||||
}}
|
||||
@value={{this.cardholderAddress.state}}
|
||||
class="subscribe-address-state"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
@type="text"
|
||||
name="promo_code"
|
||||
placeholder={{i18n "discourse_subscriptions.subscribe.promo_code"}}
|
||||
@value={{this.promoCode}}
|
||||
class="subscribe-promo-code"
|
||||
/>
|
||||
|
||||
<DButton
|
||||
@disabled={{this.loading}}
|
||||
@action={{action "stripePaymentHandler"}}
|
||||
class="btn btn-primary btn-payment"
|
||||
@label="discourse_subscriptions.plans.payment_button"
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<h2>{{i18n "discourse_subscriptions.subscribe.already_purchased"}}</h2>
|
||||
|
||||
<LinkTo
|
||||
@route="user.billing.subscriptions"
|
||||
@model={{this.currentUser.username}}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{{i18n "discourse_subscriptions.subscribe.go_to_billing"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
import RouteTemplate from "ember-route-template";
|
||||
import LoginRequired from "../components/login-required";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<div class="container">
|
||||
{{#if @controller.currentUser}}
|
||||
{{@controller.pricingTable}}
|
||||
{{else}}
|
||||
<LoginRequired />
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
);
|
|
@ -1,7 +0,0 @@
|
|||
<div class="container">
|
||||
{{#if this.currentUser}}
|
||||
{{this.pricingTable}}
|
||||
{{else}}
|
||||
<LoginRequired />
|
||||
{{/if}}
|
||||
</div>
|
|
@ -0,0 +1,35 @@
|
|||
import { LinkTo } from "@ember/routing";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import MobileNav from "discourse/components/mobile-nav";
|
||||
import bodyClass from "discourse/helpers/body-class";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
{{bodyClass "user-billing-page"}}
|
||||
|
||||
<section class="user-secondary-navigation">
|
||||
<MobileNav
|
||||
@desktopClass="action-list nav-stacked"
|
||||
@currentPath={{@controller.router._router.currentPath}}
|
||||
class="activity-nav"
|
||||
>
|
||||
<li>
|
||||
<LinkTo @route="user.billing.subscriptions">
|
||||
{{i18n "discourse_subscriptions.navigation.subscriptions"}}
|
||||
</LinkTo>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<LinkTo @route="user.billing.payments">
|
||||
{{i18n "discourse_subscriptions.navigation.payments"}}
|
||||
</LinkTo>
|
||||
</li>
|
||||
</MobileNav>
|
||||
</section>
|
||||
|
||||
<section class="user-content">
|
||||
{{outlet}}
|
||||
</section>
|
||||
</template>
|
||||
);
|
|
@ -1,25 +0,0 @@
|
|||
{{body-class "user-billing-page"}}
|
||||
|
||||
<section class="user-secondary-navigation">
|
||||
<MobileNav
|
||||
@desktopClass="action-list nav-stacked"
|
||||
@currentPath={{this.router._router.currentPath}}
|
||||
class="activity-nav"
|
||||
>
|
||||
<li>
|
||||
<LinkTo @route="user.billing.subscriptions">
|
||||
{{i18n "discourse_subscriptions.navigation.subscriptions"}}
|
||||
</LinkTo>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<LinkTo @route="user.billing.payments">
|
||||
{{i18n "discourse_subscriptions.navigation.payments"}}
|
||||
</LinkTo>
|
||||
</li>
|
||||
</MobileNav>
|
||||
</section>
|
||||
|
||||
<section class="user-content">
|
||||
{{outlet}}
|
||||
</section>
|
|
@ -0,0 +1,3 @@
|
|||
import RouteTemplate from "ember-route-template";
|
||||
|
||||
export default RouteTemplate(<template>BILLING INDEX</template>);
|
|
@ -1 +0,0 @@
|
|||
BILLING INDEX
|
|
@ -0,0 +1,31 @@
|
|||
import RouteTemplate from "ember-route-template";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import formatCurrency from "../../../helpers/format-currency";
|
||||
import formatUnixDate from "../../../helpers/format-unix-date";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
{{#if @controller.model}}
|
||||
<table class="table discourse-subscriptions-user-table">
|
||||
<thead>
|
||||
<th>{{i18n "discourse_subscriptions.user.payments.id"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.payments.amount"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.payments.created_at"}}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @controller.model as |payment|}}
|
||||
<tr>
|
||||
<td>{{payment.id}}</td>
|
||||
<td>{{formatCurrency payment.currency payment.amountDollars}}</td>
|
||||
<td>{{formatUnixDate payment.created}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="alert alert-info">
|
||||
{{i18n "discourse_subscriptions.user.payments_help"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
);
|
|
@ -1,22 +0,0 @@
|
|||
{{#if this.model}}
|
||||
<table class="table discourse-subscriptions-user-table">
|
||||
<thead>
|
||||
<th>{{i18n "discourse_subscriptions.user.payments.id"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.payments.amount"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.payments.created_at"}}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |payment|}}
|
||||
<tr>
|
||||
<td>{{payment.id}}</td>
|
||||
<td>{{format-currency payment.currency payment.amountDollars}}</td>
|
||||
<td>{{format-unix-date payment.created}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="alert alert-info">
|
||||
{{i18n "discourse_subscriptions.user.payments_help"}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -0,0 +1,28 @@
|
|||
import RouteTemplate from "ember-route-template";
|
||||
import SaveControls from "discourse/components/save-controls";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import SubscribeCard from "../../../../components/subscribe-card";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<h3>{{i18n
|
||||
"discourse_subscriptions.user.subscriptions.update_card.heading"
|
||||
sub_id=@controller.model
|
||||
}}</h3>
|
||||
|
||||
<div class="form-vertical">
|
||||
<div class="control-group">
|
||||
<SubscribeCard
|
||||
@cardElement={{@controller.cardElement}}
|
||||
class="input-xxlarge"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SaveControls
|
||||
@action={{@controller.updatePaymentMethod}}
|
||||
@saved={{@controller.saved}}
|
||||
@saveDisabled={{@controller.loading}}
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
);
|
|
@ -1,16 +0,0 @@
|
|||
<h3>{{i18n
|
||||
"discourse_subscriptions.user.subscriptions.update_card.heading"
|
||||
sub_id=this.model
|
||||
}}</h3>
|
||||
|
||||
<div class="form-vertical">
|
||||
<div class="control-group">
|
||||
<SubscribeCard @cardElement={{this.cardElement}} class="input-xxlarge" />
|
||||
</div>
|
||||
|
||||
<SaveControls
|
||||
@action={{action "updatePaymentMethod"}}
|
||||
@saved={{this.saved}}
|
||||
@saveDisabled={{this.loading}}
|
||||
/>
|
||||
</div>
|
|
@ -0,0 +1,70 @@
|
|||
import RouteTemplate from "ember-route-template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import loadingSpinner from "discourse/helpers/loading-spinner";
|
||||
import routeAction from "discourse/helpers/route-action";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import formatUnixDate from "../../../../helpers/format-unix-date";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
{{#if @controller.model}}
|
||||
<table class="table discourse-subscriptions-user-table">
|
||||
<thead>
|
||||
<th>{{i18n "discourse_subscriptions.user.subscriptions.id"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.plans.product"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.plans.rate"}}</th>
|
||||
<th>{{i18n
|
||||
"discourse_subscriptions.user.subscriptions.discounted"
|
||||
}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.subscriptions.status"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.subscriptions.renews"}}</th>
|
||||
<th>{{i18n
|
||||
"discourse_subscriptions.user.subscriptions.created_at"
|
||||
}}</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @controller.model as |subscription|}}
|
||||
<tr>
|
||||
<td>{{subscription.id}}</td>
|
||||
<td>{{subscription.product.name}}</td>
|
||||
<td>{{subscription.plan.subscriptionRate}}</td>
|
||||
<td>{{subscription.discounted}}</td>
|
||||
<td>{{subscription.status}}</td>
|
||||
<td>{{subscription.endDate}}</td>
|
||||
<td>{{formatUnixDate subscription.created}}</td>
|
||||
<td class="td-right">
|
||||
{{#if subscription.loading}}
|
||||
{{loadingSpinner size="small"}}
|
||||
{{else}}
|
||||
{{#if subscription.canceled_at}}
|
||||
<DButton
|
||||
@disabled={{subscription.canceled_at}}
|
||||
@label="discourse_subscriptions.user.subscriptions.cancelled"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{routeAction "updateCard" subscription.id}}
|
||||
@icon="far-pen-to-square"
|
||||
class="btn no-text btn-icon"
|
||||
/>
|
||||
<DButton
|
||||
class="btn-danger btn no-text btn-icon"
|
||||
@icon="trash-can"
|
||||
@disabled={{subscription.canceled_at}}
|
||||
@action={{routeAction "cancelSubscription" subscription}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="alert alert-info">
|
||||
{{i18n "discourse_subscriptions.user.subscriptions_help"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
);
|
|
@ -1,55 +0,0 @@
|
|||
{{#if this.model}}
|
||||
<table class="table discourse-subscriptions-user-table">
|
||||
<thead>
|
||||
<th>{{i18n "discourse_subscriptions.user.subscriptions.id"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.plans.product"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.plans.rate"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.subscriptions.discounted"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.subscriptions.status"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.subscriptions.renews"}}</th>
|
||||
<th>{{i18n "discourse_subscriptions.user.subscriptions.created_at"}}</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |subscription|}}
|
||||
<tr>
|
||||
<td>{{subscription.id}}</td>
|
||||
<td>{{subscription.product.name}}</td>
|
||||
<td>{{subscription.plan.subscriptionRate}}</td>
|
||||
<td>{{subscription.discounted}}</td>
|
||||
<td>{{subscription.status}}</td>
|
||||
<td>{{subscription.endDate}}</td>
|
||||
<td>{{format-unix-date subscription.created}}</td>
|
||||
<td class="td-right">
|
||||
{{#if subscription.loading}}
|
||||
{{loading-spinner size="small"}}
|
||||
{{else}}
|
||||
{{#if subscription.canceled_at}}
|
||||
<DButton
|
||||
@disabled={{subscription.canceled_at}}
|
||||
@label="discourse_subscriptions.user.subscriptions.cancelled"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{route-action "updateCard" subscription.id}}
|
||||
@icon="far-pen-to-square"
|
||||
class="btn no-text btn-icon"
|
||||
/>
|
||||
<DButton
|
||||
class="btn-danger btn no-text btn-icon"
|
||||
@icon="trash-can"
|
||||
@disabled={{subscription.canceled_at}}
|
||||
@action={{route-action "cancelSubscription" subscription}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="alert alert-info">
|
||||
{{i18n "discourse_subscriptions.user.subscriptions_help"}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,3 +1,4 @@
|
|||
@use "lib/viewport";
|
||||
/* stylelint-disable scss/no-global-function-names */
|
||||
.subscription-campaign-sidebar {
|
||||
#main-outlet
|
||||
|
@ -106,7 +107,7 @@ body.archetype-regular {
|
|||
box-shadow: 5px 5px
|
||||
var(--discourse_subscriptions_campaign_banner_shadow_color);
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
@include viewport.until(md) {
|
||||
width: 98%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -131,7 +132,7 @@ body.archetype-regular {
|
|||
padding: 2em;
|
||||
background-color: var(--primary-very-low);
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
@include viewport.until(md) {
|
||||
width: calc(100% - 4em);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -143,7 +144,7 @@ body.archetype-regular {
|
|||
font-size: $font-up-4;
|
||||
margin: 0;
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
@include viewport.until(md) {
|
||||
font-size: $font-up-3;
|
||||
}
|
||||
}
|
||||
|
@ -157,7 +158,7 @@ body.archetype-regular {
|
|||
width: 100%;
|
||||
margin: 0.25em 0 1em 0;
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
@include viewport.until(md) {
|
||||
font-size: $font-down-1;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -194,7 +195,7 @@ body.archetype-regular {
|
|||
flex-flow: column;
|
||||
justify-content: center;
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
@include viewport.until(md) {
|
||||
width: calc(100% - 4em);
|
||||
}
|
||||
|
||||
|
@ -291,7 +292,7 @@ html:not(.mobile-view) .subscriptions-campaign-topic-footer .campaign-banner {
|
|||
margin-top: 2em;
|
||||
width: calc(var(--d-max-width) * 0.87);
|
||||
|
||||
@include breakpoint(large) {
|
||||
@include viewport.until(lg) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
@ -302,7 +303,7 @@ html:not(.mobile-view) .subscriptions-campaign-topic-footer .campaign-banner {
|
|||
}
|
||||
|
||||
// Topic Footer Version + Sidebar visible
|
||||
@media screen and (max-width: 1285px) {
|
||||
@media screen and (width <= 1285px) {
|
||||
html:not(.mobile-view)
|
||||
body.has-sidebar-page
|
||||
.subscriptions-campaign-topic-footer
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
@use "lib/viewport";
|
||||
|
||||
.discourse-subscriptions-section-columns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
|
||||
@include breakpoint(medium) {
|
||||
@include viewport.until(lg) {
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0.5em;
|
||||
|
@ -22,7 +24,7 @@
|
|||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
@include breakpoint(medium) {
|
||||
@include viewport.until(lg) {
|
||||
min-width: 100%;
|
||||
|
||||
&:last-child {
|
||||
|
@ -68,7 +70,7 @@
|
|||
align-self: flex-end;
|
||||
font-size: $font-down-1;
|
||||
|
||||
@include breakpoint(large) {
|
||||
@include viewport.until(lg) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
@media all and (min-width: 1350px) {
|
||||
@media all and (width >= 1350px) {
|
||||
.address-fields {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@discourse/lint-configs": "2.11.1",
|
||||
"ember-template-lint": "7.0.1",
|
||||
"eslint": "9.22.0",
|
||||
"@discourse/lint-configs": "2.20.0",
|
||||
"ember-template-lint": "7.7.0",
|
||||
"eslint": "9.27.0",
|
||||
"prettier": "3.5.3",
|
||||
"stylelint": "16.16.0"
|
||||
"stylelint": "16.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22",
|
||||
|
|
830
pnpm-lock.yaml
830
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue