This commit is contained in:
Jarek Radosz 2025-05-28 11:43:29 +02:00 committed by GitHub
commit 5060910450
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 2389 additions and 2072 deletions

View File

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

View File

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

View File

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

View File

@ -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(),
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">
&#x2713;
{{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>
}

View File

@ -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">
&#x2713;
{{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}}

View File

@ -1,5 +0,0 @@
import Component from "@ember/component";
import { classNames } from "@ember-decorators/component";
@classNames("product")
export default class ProductItem extends Component {}

View File

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

View File

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

View File

@ -28,4 +28,8 @@ export default class SubscribeCard extends Component {
didDestroyElement() {
super.didDestroyElement(...arguments);
}
<template>
<div id="card-element"></div>
</template>
}

View File

@ -1 +0,0 @@
<div id="card-element"></div>

View File

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

View File

@ -1 +0,0 @@
<CampaignBanner @connectorName="above-main-container" />

View File

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

View File

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

View File

@ -1 +0,0 @@
<CampaignBanner @connectorName="after-topic-footer-buttons" />

View File

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

View File

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

View File

@ -1 +0,0 @@
<CampaignBanner @connectorName="before-topic-list" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import RouteTemplate from "ember-route-template";
export default RouteTemplate(<template>{{outlet}}</template>);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +0,0 @@
<div class="container">
<div class="title-wrapper">
<h1>
{{i18n "discourse_subscriptions.subscribe.title"}}
</h1>
</div>
<hr />
{{outlet}}
</div>

View File

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

View File

@ -1,5 +0,0 @@
{{#unless this.isLoggedIn}}
<LoginRequired />
{{/unless}}
<ProductList @products={{this.model}} @isLoggedIn={{this.isLoggedIn}} />

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
<div class="container">
{{#if this.currentUser}}
{{this.pricingTable}}
{{else}}
<LoginRequired />
{{/if}}
</div>

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import RouteTemplate from "ember-route-template";
export default RouteTemplate(<template>BILLING INDEX</template>);

View File

@ -1 +0,0 @@
BILLING INDEX

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",

File diff suppressed because it is too large Load Diff