Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions addon/adapters/campaign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './storefront';
1 change: 1 addition & 0 deletions addon/adapters/customer-segment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './storefront';
1 change: 1 addition & 0 deletions addon/adapters/promotion-code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './storefront';
1 change: 1 addition & 0 deletions addon/adapters/promotion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './storefront';
80 changes: 80 additions & 0 deletions addon/components/modals/campaign-form.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container space-y-3">
{{#if this.hasAudienceCount}}
<div class="text-sm text-gray-600 dark:text-gray-300">{{t "storefront.promotions.campaigns.audience-count" count=this.audienceCount}}</div>
{{/if}}
<InputGroup @name={{t "storefront.promotions.common.name"}} @value={{this.campaign.name}} />
<InputGroup @name={{t "storefront.promotions.campaigns.title-label"}} @value={{this.campaign.title}} />
<InputGroup @name={{t "storefront.promotions.campaigns.body-label"}}>
<Textarea @value={{this.campaign.body}} class="form-input w-full" rows="3" />
</InputGroup>

<InputGroup @name={{t "storefront.promotions.campaigns.audience"}}>
<ModelSelect
@modelName="customer-segment"
@selectedModel={{this.segment}}
@query={{hash owner=this.campaign.owner_uuid}}
@placeholder={{t "storefront.promotions.common.all-customers"}}
@triggerClass="form-select form-input"
@renderInPlace={{true}}
@infiniteScroll={{false}}
@allowClear={{true}}
@onChange={{this.setSegment}}
as |segment|
>
{{segment.name}}
</ModelSelect>
</InputGroup>

<InputGroup @name={{t "storefront.promotions.campaigns.channels"}}>
<div class="flex space-x-6">
<Checkbox @value={{this.hasChannel "push"}} @label={{t "storefront.promotions.campaigns.channel-push"}} @onToggle={{fn this.toggleChannel "push"}} />
<Checkbox @value={{this.hasChannel "inbox"}} @label={{t "storefront.promotions.campaigns.channel-inbox"}} @onToggle={{fn this.toggleChannel "inbox"}} />
</div>
</InputGroup>

<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<InputGroup @name={{t "storefront.promotions.campaigns.promotion"}} @helpText={{t "storefront.promotions.campaigns.promotion-help"}}>
<ModelSelect
@modelName="promotion"
@selectedModel={{this.promotion}}
@query={{hash owner=this.campaign.owner_uuid}}
@placeholder={{t "storefront.promotions.common.none"}}
@triggerClass="form-select form-input"
@renderInPlace={{true}}
@infiniteScroll={{false}}
@allowClear={{true}}
@onChange={{this.setPromotion}}
as |promotion|
>
{{promotion.name}}
</ModelSelect>
</InputGroup>
{{#unless this.promotion}}
<InputGroup @name={{t "storefront.promotions.campaigns.link-url"}}>
<Input class="form-input w-full" placeholder="https://" @value={{this.linkUrl}} {{on "change" this.setLinkUrl}} />
</InputGroup>
{{/unless}}
</div>

{{#if this.campaign.isEditable}}
<InputGroup @name={{t "storefront.promotions.campaigns.when"}}>
<div class="flex space-x-2">
{{#each this.whenOptions as |option|}}
<Button
@size="sm"
@type={{if (eq this.when option) "primary" "default"}}
@text={{t (concat "storefront.promotions.campaigns.when-" option)}}
@onClick={{fn this.setWhen option}}
/>
{{/each}}
</div>
</InputGroup>
{{#if (eq this.when "schedule")}}
<InputGroup @name={{t "storefront.promotions.campaigns.send-at"}}>
<DateTimeInput class="form-input w-full" @value={{this.campaign.send_at}} @onUpdate={{fn (mut this.campaign.send_at)}} />
</InputGroup>
{{/if}}
{{/if}}
</div>
</Modal::Default>
83 changes: 83 additions & 0 deletions addon/components/modals/campaign-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';

/**
* Create / edit a campaign: message, audience, channels, deep link and when to send.
*/
export default class ModalsCampaignFormComponent extends Component {
@service store;
@service fetch;
@service modalsManager;
@tracked segment = null;
@tracked promotion = null;
@tracked audienceCount = null;

whenOptions = ['draft', 'now', 'schedule'];

constructor() {
super(...arguments);
this.load();
}

get campaign() {
return this.args.options.campaign;
}

get when() {
return this.args.options.when;
}

get hasAudienceCount() {
return this.audienceCount !== null;
}

get linkUrl() {
return this.campaign.action?.type === 'url' ? this.campaign.action.url : '';
}

hasChannel = (channel) => (this.campaign.channels ?? []).includes(channel);

async load() {
if (this.campaign.segment_uuid) {
this.segment = await this.store.findRecord('customer-segment', this.campaign.segment_uuid).catch(() => null);
}
if (this.campaign.promotion_uuid) {
this.promotion = await this.store.findRecord('promotion', this.campaign.promotion_uuid).catch(() => null);
}
if (!this.campaign.isNew) {
const { count } = await this.fetch.get(`campaigns/${this.campaign.id}/audience`, {}, { namespace: 'storefront/int/v1' }).catch(() => ({ count: null }));
this.audienceCount = count;
}
}

@action setWhen(when) {
this.modalsManager.setOption('when', when);
}

@action setSegment(segment) {
this.segment = segment;
this.campaign.segment_uuid = segment?.id ?? null;
}

@action setPromotion(promotion) {
this.promotion = promotion;
this.campaign.promotion_uuid = promotion?.id ?? null;
this.campaign.action = promotion ? { type: 'promotion', id: promotion.public_id } : null;
}

@action setLinkUrl({ target }) {
this.campaign.action = target.value ? { type: 'url', url: target.value } : null;
}

@action toggleChannel(channel, enabled) {
const channels = new Set(this.campaign.channels ?? []);
if (enabled) {
channels.add(channel);
} else {
channels.delete(channel);
}
this.campaign.channels = [...channels];
}
}
14 changes: 14 additions & 0 deletions addon/components/modals/create-notification-channel.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@
<div class="input-group">
<Checkbox @value={{value}} @label={{humanize key}} @onToggle={{fn @options.setConfigKey key}} />
</div>
{{else if (eq key "environment")}}
<InputGroup @name={{humanize key}} @helpText={{t "storefront.component.modals.create-notification-channel.environment-help-text"}}>
<Select
class="w-full"
@options={{array "auto" "production" "sandbox"}}
@value={{value}}
@onSelect={{fn @options.setConfigKey key}}
@humanize={{true}}
/>
</InputGroup>
{{else if (eq key "android_channel_id")}}
<InputGroup @name={{humanize key}} @helpText={{t "storefront.component.modals.create-notification-channel.android-channel-id-help-text"}}>
<Input class="form-input w-full" placeholder={{humanize key}} @value={{value}} {{on "blur" (fn @options.setConfigKey key)}} />
</InputGroup>
{{else if (or (eq key "private_key_content") (eq key "firebase_credentials_json"))}}
<InputGroup @name={{humanize key}}>
<Textarea class="form-input w-full" rows="6" placeholder={{humanize key}} @value={{value}} {{on "blur" (fn @options.setConfigKey key)}} />
Expand Down
31 changes: 31 additions & 0 deletions addon/components/modals/customer-segment-form.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container space-y-3">
<InputGroup @name={{t "storefront.promotions.common.name"}} @value={{this.segment.name}} />
<InputGroup @name={{t "storefront.promotions.common.description"}}>
<Textarea @value={{this.segment.description}} class="form-input w-full" rows="2" />
</InputGroup>

<div class="pt-2 border-t dark:border-gray-700">
<div class="flex items-center justify-between mb-2">
<h4 class="font-semibold text-sm dark:text-white">{{t "storefront.promotions.segments.rules"}}</h4>
{{#if this.hasMatching}}
<Badge @status="info" @hideStatusDot={{true}}>{{t "storefront.promotions.segments.matching" count=this.matching}}</Badge>
{{/if}}
</div>
<p class="text-xs text-gray-500 mb-2">{{t "storefront.promotions.segments.rules-help"}}</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
{{#each this.numberRules as |rule|}}
<InputGroup @name={{t rule.label}}>
<Input @type="number" min="0" class="form-input w-full" @value={{get this.rules rule.key}} {{on "input" (fn this.setNumberRule rule.key)}} />
</InputGroup>
{{/each}}
<InputGroup @name={{t "storefront.promotions.segments.min-spent"}}>
<MoneyInput class="w-full" @currency={{@options.currency}} @value={{this.rules.min_spent}} @canSelectCurrency={{false}} @onChange={{fn this.setMoneyRule "min_spent"}} />
</InputGroup>
</div>
<div class="mt-3">
<Toggle @isToggled={{this.rules.has_push_device}} @onToggle={{fn this.setBooleanRule "has_push_device"}} @label={{t "storefront.promotions.segments.has-push-device"}} />
</div>
</div>
</div>
</Modal::Default>
67 changes: 67 additions & 0 deletions addon/components/modals/customer-segment-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { debounce } from '@ember/runloop';

/**
* Create / edit a customer segment with a live count of matching customers.
*/
export default class ModalsCustomerSegmentFormComponent extends Component {
@service fetch;
@tracked matching = null;

numberRules = ['min_orders', 'max_orders', 'ordered_within_days', 'not_ordered_within_days', 'joined_within_days'].map((key) => ({
key,
label: `storefront.promotions.segments.${key.replace(/_/g, '-')}`,
}));

constructor() {
super(...arguments);
this.refreshCount();
}

get segment() {
return this.args.options.segment;
}

get hasMatching() {
return this.matching !== null;
}

get rules() {
return this.segment.rules ?? {};
}

setRule(key, value) {
const rules = { ...this.rules };
if (value === null || value === '' || value === false) {
delete rules[key];
} else {
rules[key] = value;
}
this.segment.rules = rules;
debounce(this, this.refreshCount, 400);
}

@action setNumberRule(key, { target }) {
this.setRule(key, target.value === '' ? null : Number(target.value));
}

@action setMoneyRule(key, value) {
this.setRule(key, value || null);
}

@action setBooleanRule(key, value) {
this.setRule(key, value);
}

async refreshCount() {
try {
const { count } = await this.fetch.post('customer-segments/preview', { owner_uuid: this.segment.owner_uuid, rules: this.rules }, { namespace: 'storefront/int/v1' });
this.matching = count;
} catch {
this.matching = null;
}
}
}
27 changes: 27 additions & 0 deletions addon/components/modals/promotion-announce.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container space-y-3">
<p class="text-sm text-gray-600 dark:text-gray-300">{{t "storefront.promotions.announce.help"}}</p>
<InputGroup @name={{t "storefront.promotions.announce.segment"}}>
<ModelSelect
@modelName="customer-segment"
@selectedModel={{@options.announcement.segment}}
@placeholder={{t "storefront.promotions.announce.segment-placeholder"}}
@triggerClass="form-select form-input"
@renderInPlace={{true}}
@infiniteScroll={{false}}
@allowClear={{true}}
@onChange={{fn (mut @options.announcement.segment)}}
as |segment|
>
{{segment.name}}
</ModelSelect>
</InputGroup>
<InputGroup @name={{t "storefront.promotions.announce.message-title"}} @value={{@options.announcement.title}} />
<InputGroup @name={{t "storefront.promotions.announce.message-body"}}>
<Textarea @value={{@options.announcement.body}} class="form-input w-full" rows="3" />
</InputGroup>
<InputGroup @name={{t "storefront.promotions.announce.send-at"}}>
<DateTimeInput class="form-input w-full" @value={{@options.announcement.send_at}} @onUpdate={{fn (mut @options.announcement.send_at)}} />
</InputGroup>
</div>
</Modal::Default>
65 changes: 65 additions & 0 deletions addon/components/modals/promotion-codes.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container space-y-4">
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
<InputGroup @name={{t "storefront.promotions.codes.count"}}>
<Input @type="number" min="1" max="1000" class="form-input w-full" @value={{this.count}} {{on "input" (fn this.setValue "count")}} />
</InputGroup>
<InputGroup @name={{t "storefront.promotions.codes.prefix"}}>
<Input class="form-input w-full uppercase" @value={{this.prefix}} {{on "input" (fn this.setValue "prefix")}} />
</InputGroup>
<InputGroup @name={{t "storefront.promotions.codes.length"}}>
<Input @type="number" min="6" max="16" class="form-input w-full" @value={{this.length}} {{on "input" (fn this.setValue "length")}} />
</InputGroup>
<InputGroup @name={{t "storefront.promotions.codes.usage-limit"}} @helpText={{t "storefront.promotions.codes.usage-limit-help"}}>
<Input @type="number" min="1" class="form-input w-full" @value={{this.usageLimit}} {{on "input" (fn this.setValue "usageLimit")}} />
</InputGroup>
</div>
<InputGroup @name={{t "storefront.promotions.codes.specific-code"}}>
<Input class="form-input w-full uppercase" placeholder={{t "storefront.promotions.codes.specific-code-placeholder"}} @value={{this.specificCode}} {{on "input" (fn this.setValue "specificCode")}} />
</InputGroup>
<div class="flex justify-end">
<Button
@type="primary"
@icon="wand-magic-sparkles"
@text={{t "storefront.promotions.codes.generate"}}
@isLoading={{this.isGenerating}}
@onClick={{this.generate}}
@permission="storefront generate-codes promotion"
/>
</div>

<div class="border rounded-md dark:border-gray-700 max-h-80 overflow-y-auto">
<table class="w-full text-sm">
<thead class="text-left text-xs uppercase text-gray-500">
<tr>
<th class="px-3 py-2">{{t "storefront.promotions.codes.code"}}</th>
<th class="px-3 py-2">{{t "storefront.promotions.codes.used"}}</th>
<th class="px-3 py-2">{{t "storefront.promotions.common.status"}}</th>
<th class="px-3 py-2"></th>
</tr>
</thead>
<tbody>
{{#each this.codes as |code|}}
<tr class="border-t dark:border-gray-700">
<td class="px-3 py-2 font-mono"><ClickToCopy @value={{code.code}}>{{code.code}}</ClickToCopy></td>
<td class="px-3 py-2">{{code.times_used}}{{#if code.usage_limit}} / {{code.usage_limit}}{{/if}}</td>
<td class="px-3 py-2"><Badge @status={{code.status}} @hideStatusDot={{true}} /></td>
<td class="px-3 py-2 text-right">
<Button
@size="xs"
@text={{if (eq code.status "active") (t "storefront.promotions.codes.disable") (t "storefront.promotions.codes.enable")}}
@onClick={{fn this.toggleCode code}}
@permission="storefront update promotion-code"
/>
</td>
</tr>
{{else}}
<tr>
<td colspan="4" class="px-3 py-4 text-center text-gray-500">{{t "storefront.promotions.codes.empty"}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</Modal::Default>
Loading
Loading