@burdenoff/microfe-billing 2026.614.1 → 2026.623.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing/BillingAdminRoutes.js +7 -6
- package/dist/billing/BillingAdminRoutes.js.map +1 -1
- package/dist/billing/BillingRoot.js +1 -1
- package/dist/billing/BillingRoot.js.map +1 -1
- package/dist/billing/modules/addons/components/FloatingCartDrawer.js +22 -22
- package/dist/billing/modules/addons/components/FloatingCartDrawer.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonCreatePage.js +31 -31
- package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonDetailPage.js +39 -39
- package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonEditPage.js +29 -29
- package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsListPage.js +91 -91
- package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js +40 -40
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js +115 -115
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js +75 -72
- package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/RefundsPage.js +126 -126
- package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/UserRefundsPage.js +45 -45
- package/dist/billing/modules/billing/pages/UserRefundsPage.js.map +1 -1
- package/dist/billing/modules/checkout/components/BillingAccountSelector.js +25 -25
- package/dist/billing/modules/checkout/components/BillingAccountSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js +31 -35
- package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/checkout/components/CreditsAmountInput.js +9 -9
- package/dist/billing/modules/checkout/components/CreditsAmountInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/OrderSummary.js +31 -31
- package/dist/billing/modules/checkout/components/OrderSummary.js.map +1 -1
- package/dist/billing/modules/checkout/components/PromoCodeInput.js +8 -8
- package/dist/billing/modules/checkout/components/PromoCodeInput.js.map +1 -1
- package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js +5 -5
- package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js.map +1 -1
- package/dist/billing/modules/checkout/components/SimpleAutoPayToggle.js +9 -9
- package/dist/billing/modules/checkout/components/SimpleAutoPayToggle.js.map +1 -1
- package/dist/billing/modules/checkout/components/StripePaymentForm.js +21 -21
- package/dist/billing/modules/checkout/components/StripePaymentForm.js.map +1 -1
- package/dist/billing/modules/checkout/components/SubscriptionSelector.js +11 -11
- package/dist/billing/modules/checkout/components/SubscriptionSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/UnifiedPaymentMethodSelector.js +31 -31
- package/dist/billing/modules/checkout/components/UnifiedPaymentMethodSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/WorkspaceQuotaAssignmentModal.js +23 -23
- package/dist/billing/modules/checkout/components/WorkspaceQuotaAssignmentModal.js.map +1 -1
- package/dist/billing/modules/checkout/pages/CheckoutPage.js +91 -91
- package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js +62 -62
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponsListPage.js +72 -60
- package/dist/billing/modules/coupons/pages/CouponsListPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js +66 -66
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/EditCouponPage.js +46 -46
- package/dist/billing/modules/coupons/pages/EditCouponPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/AdminCreditsPage.js +19 -19
- package/dist/billing/modules/credits/pages/AdminCreditsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditTransactionsPage.js +37 -37
- package/dist/billing/modules/credits/pages/CreditTransactionsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js +90 -90
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditsPage.js +41 -41
- package/dist/billing/modules/credits/pages/CreditsPage.js.map +1 -1
- package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js +31 -31
- package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js +35 -35
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/BillingAccountCard.js +24 -24
- package/dist/billing/modules/dashboard/components/BillingAccountCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js +99 -99
- package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js +23 -23
- package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js +22 -22
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/pages/OverviewPage.js +58 -58
- package/dist/billing/modules/dashboard/pages/OverviewPage.js.map +1 -1
- package/dist/billing/modules/discounts/pages/DiscountsListPage.js +126 -126
- package/dist/billing/modules/discounts/pages/DiscountsListPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +83 -79
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanAssignFreePage.js +16 -16
- package/dist/billing/modules/plans/pages/PlanAssignFreePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js +40 -40
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanCreatePage.js +35 -35
- package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanDetailPage.js +57 -57
- package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanEditPage.js +35 -35
- package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js +57 -57
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansListPage.js +52 -52
- package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
- package/dist/billing/modules/promotions/pages/CreatePromotionPage.js +40 -39
- package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -1
- package/dist/billing/modules/promotions/pages/PromotionsListPage.js +22 -19
- package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js +34 -34
- package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js +47 -47
- package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js +39 -39
- package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js +52 -52
- package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js +23 -23
- package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js +259 -246
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js +88 -85
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js +51 -51
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
- package/dist/billing/modules/usage/pages/UsagePage.js +206 -196
- package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
- package/dist/billing/shared/components/ServerError.js +4 -4
- package/dist/billing/shared/components/ServerError.js.map +1 -1
- package/dist/billing/shared/components/StatCard.js +11 -33
- package/dist/billing/shared/components/StatCard.js.map +1 -1
- package/dist/billing/shared/ui/Card.js +15 -0
- package/dist/billing/shared/ui/Card.js.map +1 -0
- package/dist/billing/shared/ui/MetricCard.js +44 -0
- package/dist/billing/shared/ui/MetricCard.js.map +1 -0
- package/dist/billing/shared/ui/ProgressBar.js +30 -0
- package/dist/billing/shared/ui/ProgressBar.js.map +1 -0
- package/dist/billing/shared/ui/SectionHeader.js +2 -0
- package/dist/billing/shared/ui/StatusPill.js +28 -0
- package/dist/billing/shared/ui/StatusPill.js.map +1 -0
- package/dist/billing/shared/ui/index.js +5 -0
- package/dist/billing/shared/utils/tokens.js +13 -30
- package/dist/billing/shared/utils/tokens.js.map +1 -1
- package/package.json +1 -1
|
@@ -42,7 +42,8 @@ var f = [
|
|
|
42
42
|
children: [/* @__PURE__ */ u("button", {
|
|
43
43
|
type: "button",
|
|
44
44
|
onClick: () => p("/promotions"),
|
|
45
|
-
|
|
45
|
+
"aria-label": "Back to promotions",
|
|
46
|
+
className: "p-2 rounded-button hover:bg-bg-sunken text-text-muted hover:text-text-primary transition-colors",
|
|
46
47
|
children: /* @__PURE__ */ u(o, { className: "size-5" })
|
|
47
48
|
}), /* @__PURE__ */ u(t, {
|
|
48
49
|
title: "Create Promotion",
|
|
@@ -78,15 +79,15 @@ var f = [
|
|
|
78
79
|
className: "space-y-5",
|
|
79
80
|
children: [
|
|
80
81
|
/* @__PURE__ */ d("div", {
|
|
81
|
-
className: "bg-
|
|
82
|
+
className: "bg-bg-surface border border-border-subtle rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]",
|
|
82
83
|
children: [
|
|
83
84
|
/* @__PURE__ */ u("h3", {
|
|
84
|
-
className: "font-semibold text-
|
|
85
|
+
className: "font-semibold text-text-primary",
|
|
85
86
|
children: "Promotion Details"
|
|
86
87
|
}),
|
|
87
88
|
/* @__PURE__ */ d("div", { children: [
|
|
88
89
|
/* @__PURE__ */ u("label", {
|
|
89
|
-
className: "block text-sm font-medium text-
|
|
90
|
+
className: "block text-sm font-medium text-text-primary mb-1",
|
|
90
91
|
children: "Name *"
|
|
91
92
|
}),
|
|
92
93
|
/* @__PURE__ */ u("input", {
|
|
@@ -94,35 +95,35 @@ var f = [
|
|
|
94
95
|
value: x.name,
|
|
95
96
|
onChange: (e) => C("name", e.target.value),
|
|
96
97
|
placeholder: "e.g. June Launch Offer",
|
|
97
|
-
className: `w-full px-3 py-2 rounded-
|
|
98
|
+
className: `w-full px-3 py-2 rounded-input border ${v.name ? "border-destructive" : "border-border-subtle"} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`
|
|
98
99
|
}),
|
|
99
100
|
v.name && /* @__PURE__ */ u("p", {
|
|
100
|
-
className: "text-xs text-
|
|
101
|
+
className: "text-xs text-status-error-text mt-1",
|
|
101
102
|
children: v.name
|
|
102
103
|
})
|
|
103
104
|
] }),
|
|
104
105
|
/* @__PURE__ */ d("div", { children: [/* @__PURE__ */ u("label", {
|
|
105
|
-
className: "block text-sm font-medium text-
|
|
106
|
+
className: "block text-sm font-medium text-text-primary mb-1",
|
|
106
107
|
children: "Description"
|
|
107
108
|
}), /* @__PURE__ */ u("textarea", {
|
|
108
109
|
value: x.description,
|
|
109
110
|
onChange: (e) => C("description", e.target.value),
|
|
110
111
|
placeholder: "Subscribe in June and get 2 extra months free!",
|
|
111
112
|
rows: 2,
|
|
112
|
-
className: "w-full px-3 py-2 rounded-
|
|
113
|
+
className: "w-full px-3 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
|
|
113
114
|
})] })
|
|
114
115
|
]
|
|
115
116
|
}),
|
|
116
117
|
/* @__PURE__ */ d("div", {
|
|
117
|
-
className: "bg-
|
|
118
|
+
className: "bg-bg-surface border border-border-subtle rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]",
|
|
118
119
|
children: [
|
|
119
120
|
/* @__PURE__ */ u("h3", {
|
|
120
|
-
className: "font-semibold text-
|
|
121
|
+
className: "font-semibold text-text-primary",
|
|
121
122
|
children: "Bonus Configuration"
|
|
122
123
|
}),
|
|
123
124
|
/* @__PURE__ */ d("div", { children: [
|
|
124
125
|
/* @__PURE__ */ u("label", {
|
|
125
|
-
className: "block text-sm font-medium text-
|
|
126
|
+
className: "block text-sm font-medium text-text-primary mb-1",
|
|
126
127
|
children: "Bonus Months *"
|
|
127
128
|
}),
|
|
128
129
|
/* @__PURE__ */ u("input", {
|
|
@@ -131,19 +132,19 @@ var f = [
|
|
|
131
132
|
min: 1,
|
|
132
133
|
max: 12,
|
|
133
134
|
onChange: (e) => C("bonusMonths", parseInt(e.target.value) || 1),
|
|
134
|
-
className: `w-32 px-3 py-2 rounded-
|
|
135
|
+
className: `w-32 px-3 py-2 rounded-input border ${v.bonusMonths ? "border-destructive" : "border-border-subtle"} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`
|
|
135
136
|
}),
|
|
136
137
|
v.bonusMonths && /* @__PURE__ */ u("p", {
|
|
137
|
-
className: "text-xs text-
|
|
138
|
+
className: "text-xs text-status-error-text mt-1",
|
|
138
139
|
children: v.bonusMonths
|
|
139
140
|
}),
|
|
140
141
|
/* @__PURE__ */ d("div", {
|
|
141
|
-
className: "mt-2 flex items-start gap-2 p-3 bg-
|
|
142
|
-
children: [/* @__PURE__ */ u(c, { className: "size-4 shrink-0 mt-0.5 text-
|
|
142
|
+
className: "mt-2 flex items-start gap-2 p-3 bg-[var(--color-accent-soft)] rounded-lg text-sm text-text-muted",
|
|
143
|
+
children: [/* @__PURE__ */ u(c, { className: "size-4 shrink-0 mt-0.5 text-text-link" }), /* @__PURE__ */ d("span", { children: [
|
|
143
144
|
"Users who subscribe during this window get",
|
|
144
145
|
" ",
|
|
145
146
|
/* @__PURE__ */ d("strong", {
|
|
146
|
-
className: "text-
|
|
147
|
+
className: "text-text-primary",
|
|
147
148
|
children: [
|
|
148
149
|
x.bonusMonths,
|
|
149
150
|
" free renewal",
|
|
@@ -157,7 +158,7 @@ var f = [
|
|
|
157
158
|
] }),
|
|
158
159
|
/* @__PURE__ */ d("div", { children: [
|
|
159
160
|
/* @__PURE__ */ u("label", {
|
|
160
|
-
className: "block text-sm font-medium text-
|
|
161
|
+
className: "block text-sm font-medium text-text-primary mb-1",
|
|
161
162
|
children: "Max Redemptions"
|
|
162
163
|
}),
|
|
163
164
|
/* @__PURE__ */ u("input", {
|
|
@@ -166,42 +167,42 @@ var f = [
|
|
|
166
167
|
min: 1,
|
|
167
168
|
onChange: (e) => C("maxRedemptions", e.target.value),
|
|
168
169
|
placeholder: "Unlimited",
|
|
169
|
-
className: "w-48 px-3 py-2 rounded-
|
|
170
|
+
className: "w-48 px-3 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
|
|
170
171
|
}),
|
|
171
172
|
/* @__PURE__ */ u("p", {
|
|
172
|
-
className: "text-xs text-muted
|
|
173
|
+
className: "text-xs text-text-muted mt-1",
|
|
173
174
|
children: "Leave empty for no cap"
|
|
174
175
|
})
|
|
175
176
|
] })
|
|
176
177
|
]
|
|
177
178
|
}),
|
|
178
179
|
/* @__PURE__ */ d("div", {
|
|
179
|
-
className: "bg-
|
|
180
|
+
className: "bg-bg-surface border border-border-subtle rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]",
|
|
180
181
|
children: [
|
|
181
182
|
/* @__PURE__ */ u("h3", {
|
|
182
|
-
className: "font-semibold text-
|
|
183
|
+
className: "font-semibold text-text-primary",
|
|
183
184
|
children: "Active Window"
|
|
184
185
|
}),
|
|
185
186
|
/* @__PURE__ */ d("div", {
|
|
186
187
|
className: "grid grid-cols-2 gap-4",
|
|
187
188
|
children: [/* @__PURE__ */ d("div", { children: [
|
|
188
189
|
/* @__PURE__ */ u("label", {
|
|
189
|
-
className: "block text-sm font-medium text-
|
|
190
|
+
className: "block text-sm font-medium text-text-primary mb-1",
|
|
190
191
|
children: "Start Date *"
|
|
191
192
|
}),
|
|
192
193
|
/* @__PURE__ */ u("input", {
|
|
193
194
|
type: "date",
|
|
194
195
|
value: x.startsAt,
|
|
195
196
|
onChange: (e) => C("startsAt", e.target.value),
|
|
196
|
-
className: `w-full px-3 py-2 rounded-
|
|
197
|
+
className: `w-full px-3 py-2 rounded-input border ${v.startsAt ? "border-destructive" : "border-border-subtle"} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`
|
|
197
198
|
}),
|
|
198
199
|
v.startsAt && /* @__PURE__ */ u("p", {
|
|
199
|
-
className: "text-xs text-
|
|
200
|
+
className: "text-xs text-status-error-text mt-1",
|
|
200
201
|
children: v.startsAt
|
|
201
202
|
})
|
|
202
203
|
] }), /* @__PURE__ */ d("div", { children: [
|
|
203
204
|
/* @__PURE__ */ u("label", {
|
|
204
|
-
className: "block text-sm font-medium text-
|
|
205
|
+
className: "block text-sm font-medium text-text-primary mb-1",
|
|
205
206
|
children: "End Date *"
|
|
206
207
|
}),
|
|
207
208
|
/* @__PURE__ */ u("input", {
|
|
@@ -209,30 +210,30 @@ var f = [
|
|
|
209
210
|
value: x.expiresAt,
|
|
210
211
|
onChange: (e) => C("expiresAt", e.target.value),
|
|
211
212
|
min: x.startsAt,
|
|
212
|
-
className: `w-full px-3 py-2 rounded-
|
|
213
|
+
className: `w-full px-3 py-2 rounded-input border ${v.expiresAt ? "border-destructive" : "border-border-subtle"} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`
|
|
213
214
|
}),
|
|
214
215
|
v.expiresAt && /* @__PURE__ */ u("p", {
|
|
215
|
-
className: "text-xs text-
|
|
216
|
+
className: "text-xs text-status-error-text mt-1",
|
|
216
217
|
children: v.expiresAt
|
|
217
218
|
})
|
|
218
219
|
] })]
|
|
219
220
|
}),
|
|
220
221
|
/* @__PURE__ */ u("p", {
|
|
221
|
-
className: "text-xs text-muted
|
|
222
|
+
className: "text-xs text-text-muted",
|
|
222
223
|
children: "Only purchases made between these dates will receive the bonus months"
|
|
223
224
|
})
|
|
224
225
|
]
|
|
225
226
|
}),
|
|
226
227
|
/* @__PURE__ */ d("div", {
|
|
227
|
-
className: "bg-
|
|
228
|
+
className: "bg-bg-surface border border-border-subtle rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]",
|
|
228
229
|
children: [
|
|
229
230
|
/* @__PURE__ */ u("h3", {
|
|
230
|
-
className: "font-semibold text-
|
|
231
|
+
className: "font-semibold text-text-primary",
|
|
231
232
|
children: "Scope"
|
|
232
233
|
}),
|
|
233
234
|
/* @__PURE__ */ d("div", { children: [
|
|
234
235
|
/* @__PURE__ */ u("label", {
|
|
235
|
-
className: "block text-sm font-medium text-
|
|
236
|
+
className: "block text-sm font-medium text-text-primary mb-2",
|
|
236
237
|
children: "Applicable Products"
|
|
237
238
|
}),
|
|
238
239
|
/* @__PURE__ */ u("div", {
|
|
@@ -240,18 +241,18 @@ var f = [
|
|
|
240
241
|
children: f.map((e) => /* @__PURE__ */ u("button", {
|
|
241
242
|
type: "button",
|
|
242
243
|
onClick: () => w(e),
|
|
243
|
-
className: `px-3 py-1.5 rounded-lg border text-sm transition-colors ${x.applicableProductIds.includes(e) ? "border-primary bg-
|
|
244
|
+
className: `px-3 py-1.5 rounded-lg border text-sm transition-colors ${x.applicableProductIds.includes(e) ? "border-action-primary-bg bg-[var(--color-accent-soft)] text-text-link font-medium" : "border-border-subtle hover:border-action-primary-bg/50"}`,
|
|
244
245
|
children: e
|
|
245
246
|
}, e))
|
|
246
247
|
}),
|
|
247
248
|
/* @__PURE__ */ u("p", {
|
|
248
|
-
className: "text-xs text-muted
|
|
249
|
+
className: "text-xs text-text-muted mt-1",
|
|
249
250
|
children: x.applicableProductIds.length === 0 ? "No selection = applies to all products" : `Applies to: ${x.applicableProductIds.join(", ")}`
|
|
250
251
|
})
|
|
251
252
|
] }),
|
|
252
253
|
/* @__PURE__ */ d("div", { children: [
|
|
253
254
|
/* @__PURE__ */ u("label", {
|
|
254
|
-
className: "block text-sm font-medium text-
|
|
255
|
+
className: "block text-sm font-medium text-text-primary mb-1",
|
|
255
256
|
children: "Plan IDs (optional)"
|
|
256
257
|
}),
|
|
257
258
|
/* @__PURE__ */ u("input", {
|
|
@@ -259,10 +260,10 @@ var f = [
|
|
|
259
260
|
value: x.applicablePlanIds,
|
|
260
261
|
onChange: (e) => C("applicablePlanIds", e.target.value),
|
|
261
262
|
placeholder: "plan-vibecontrols-pro-monthly, plan-vibecontrols-pro-yearly",
|
|
262
|
-
className: "w-full px-3 py-2 rounded-
|
|
263
|
+
className: "w-full px-3 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary placeholder:text-text-muted text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
|
|
263
264
|
}),
|
|
264
265
|
/* @__PURE__ */ u("p", {
|
|
265
|
-
className: "text-xs text-muted
|
|
266
|
+
className: "text-xs text-text-muted mt-1",
|
|
266
267
|
children: "Comma-separated. Leave empty to apply to all plans"
|
|
267
268
|
})
|
|
268
269
|
] })
|
|
@@ -273,13 +274,13 @@ var f = [
|
|
|
273
274
|
children: [/* @__PURE__ */ u("button", {
|
|
274
275
|
type: "button",
|
|
275
276
|
onClick: () => p("/promotions"),
|
|
276
|
-
className: "px-5 py-2 rounded-
|
|
277
|
+
className: "px-5 py-2 rounded-button border border-border-subtle text-text-primary hover:bg-bg-sunken text-sm font-medium transition-colors",
|
|
277
278
|
children: "Cancel"
|
|
278
279
|
}), /* @__PURE__ */ d("button", {
|
|
279
280
|
type: "submit",
|
|
280
281
|
disabled: g,
|
|
281
|
-
className: "px-5 py-2 rounded-
|
|
282
|
-
children: [g ? /* @__PURE__ */ u("div", { className: "size-4 border-2 border-primary-
|
|
282
|
+
className: "px-5 py-2 rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover flex items-center gap-2 text-sm font-medium transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-50",
|
|
283
|
+
children: [g ? /* @__PURE__ */ u("div", { className: "size-4 border-2 border-action-primary-text border-t-transparent rounded-full animate-spin" }) : /* @__PURE__ */ u(l, { className: "size-4" }), "Create Promotion"]
|
|
283
284
|
})]
|
|
284
285
|
})
|
|
285
286
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreatePromotionPage.js","names":[],"sources":["../../../../../src/billing/modules/promotions/pages/CreatePromotionPage.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { ArrowLeft, Save, Gift, Info } from 'lucide-react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { useCreatePromotionMutation } from '../../../../generated/global-operations';\n\nconst KNOWN_PRODUCTS = ['vibecontrols', 'workspaces', 'botlit', 'fluidgrids', 'healthybowl'];\n\ninterface FormData {\n name: string;\n description: string;\n startsAt: string;\n expiresAt: string;\n bonusMonths: number;\n maxRedemptions: string;\n applicableProductIds: string[];\n applicablePlanIds: string;\n}\n\nexport const CreatePromotionPage: FC = () => {\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const [createPromotion] = useCreatePromotionMutation({ refetchQueries: ['ListPromotions'] });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});\n\n const today = new Date().toISOString().split('T')[0];\n const [form, setForm] = useState<FormData>({\n name: '',\n description: '',\n startsAt: today,\n expiresAt: '',\n bonusMonths: 1,\n maxRedemptions: '',\n applicableProductIds: [],\n applicablePlanIds: '',\n });\n\n if (!permissions.canManagePlans) {\n return <AccessDenied message=\"You don't have permission to create promotions.\" />;\n }\n\n const set = <K extends keyof FormData>(k: K, v: FormData[K]) =>\n setForm((prev) => ({ ...prev, [k]: v }));\n\n const toggleProduct = (pid: string) => {\n set(\n 'applicableProductIds',\n form.applicableProductIds.includes(pid)\n ? form.applicableProductIds.filter((p) => p !== pid)\n : [...form.applicableProductIds, pid]\n );\n };\n\n const validate = () => {\n const e: typeof errors = {};\n if (!form.name.trim()) e.name = 'Name is required';\n if (!form.startsAt) e.startsAt = 'Start date is required';\n if (!form.expiresAt) e.expiresAt = 'End date is required';\n if (form.expiresAt && form.expiresAt <= form.startsAt)\n e.expiresAt = 'End date must be after start date';\n if (form.bonusMonths < 1) e.bonusMonths = 'Must be at least 1';\n setErrors(e);\n return Object.keys(e).length === 0;\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n setIsSubmitting(true);\n try {\n const planIds = form.applicablePlanIds.split(',').flatMap((s) => {\n const trimmed = s.trim();\n return trimmed ? [trimmed] : [];\n });\n\n await createPromotion({\n variables: {\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n startsAt: new Date(form.startsAt).toISOString(),\n expiresAt: new Date(form.expiresAt).toISOString(),\n bonusMonths: form.bonusMonths,\n maxRedemptions: form.maxRedemptions ? parseInt(form.maxRedemptions) : undefined,\n applicableProductIds: form.applicableProductIds,\n applicablePlanIds: planIds,\n },\n },\n });\n navigateTo('/promotions');\n } catch (err) {\n setErrors({ name: err instanceof Error ? err.message : 'Failed to create promotion' });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div className=\"max-w-2xl mx-auto p-4 sm:p-6 space-y-6\">\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/promotions')}\n className=\"p-2 rounded-lg hover:bg-muted text-muted-foreground\"\n >\n <ArrowLeft className=\"size-5\" />\n </button>\n <PageHeader\n title=\"Create Promotion\"\n description=\"Offer bonus free renewal months to subscribers who sign up within the promotion window\"\n icon={<Gift className=\"size-6\" />}\n />\n </div>\n\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Basic info */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Promotion Details</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Name *</label>\n <input\n type=\"text\"\n value={form.name}\n onChange={(e) => set('name', e.target.value)}\n placeholder=\"e.g. June Launch Offer\"\n className={`w-full px-3 py-2 rounded-lg border ${errors.name ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.name && <p className=\"text-xs text-destructive mt-1\">{errors.name}</p>}\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Description</label>\n <textarea\n value={form.description}\n onChange={(e) => set('description', e.target.value)}\n placeholder=\"Subscribe in June and get 2 extra months free!\"\n rows={2}\n className=\"w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20\"\n />\n </div>\n </div>\n\n {/* Bonus config */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Bonus Configuration</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Bonus Months *</label>\n <input\n type=\"number\"\n value={form.bonusMonths}\n min={1}\n max={12}\n onChange={(e) => set('bonusMonths', parseInt(e.target.value) || 1)}\n className={`w-32 px-3 py-2 rounded-lg border ${errors.bonusMonths ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.bonusMonths && (\n <p className=\"text-xs text-destructive mt-1\">{errors.bonusMonths}</p>\n )}\n <div className=\"mt-2 flex items-start gap-2 p-3 bg-primary/5 rounded-lg text-sm text-muted-foreground\">\n <Info className=\"size-4 shrink-0 mt-0.5 text-primary\" />\n <span>\n Users who subscribe during this window get{' '}\n <strong className=\"text-foreground\">\n {form.bonusMonths} free renewal{form.bonusMonths !== 1 ? 's' : ''}\n </strong>{' '}\n after their first paid period — their quota resets each month as normal.\n </span>\n </div>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Max Redemptions\n </label>\n <input\n type=\"number\"\n value={form.maxRedemptions}\n min={1}\n onChange={(e) => set('maxRedemptions', e.target.value)}\n placeholder=\"Unlimited\"\n className=\"w-48 px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20\"\n />\n <p className=\"text-xs text-muted-foreground mt-1\">Leave empty for no cap</p>\n </div>\n </div>\n\n {/* Date window */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Active Window</h3>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Start Date *</label>\n <input\n type=\"date\"\n value={form.startsAt}\n onChange={(e) => set('startsAt', e.target.value)}\n className={`w-full px-3 py-2 rounded-lg border ${errors.startsAt ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.startsAt && (\n <p className=\"text-xs text-destructive mt-1\">{errors.startsAt}</p>\n )}\n </div>\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">End Date *</label>\n <input\n type=\"date\"\n value={form.expiresAt}\n onChange={(e) => set('expiresAt', e.target.value)}\n min={form.startsAt}\n className={`w-full px-3 py-2 rounded-lg border ${errors.expiresAt ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.expiresAt && (\n <p className=\"text-xs text-destructive mt-1\">{errors.expiresAt}</p>\n )}\n </div>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n Only purchases made between these dates will receive the bonus months\n </p>\n </div>\n\n {/* Scope */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Scope</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-2\">\n Applicable Products\n </label>\n <div className=\"flex flex-wrap gap-2\">\n {KNOWN_PRODUCTS.map((pid) => (\n <button\n key={pid}\n type=\"button\"\n onClick={() => toggleProduct(pid)}\n className={`px-3 py-1.5 rounded-lg border text-sm transition-colors ${\n form.applicableProductIds.includes(pid)\n ? 'border-primary bg-primary/10 text-primary font-medium'\n : 'border-border hover:border-primary/50'\n }`}\n >\n {pid}\n </button>\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground mt-1\">\n {form.applicableProductIds.length === 0\n ? 'No selection = applies to all products'\n : `Applies to: ${form.applicableProductIds.join(', ')}`}\n </p>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Plan IDs (optional)\n </label>\n <input\n type=\"text\"\n value={form.applicablePlanIds}\n onChange={(e) => set('applicablePlanIds', e.target.value)}\n placeholder=\"plan-vibecontrols-pro-monthly, plan-vibecontrols-pro-yearly\"\n className=\"w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/20\"\n />\n <p className=\"text-xs text-muted-foreground mt-1\">\n Comma-separated. Leave empty to apply to all plans\n </p>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex gap-3 justify-end\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/promotions')}\n className=\"px-5 py-2 rounded-lg border border-border text-foreground hover:bg-muted text-sm\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting}\n className=\"px-5 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 text-sm disabled:opacity-50\"\n >\n {isSubmitting ? (\n <div className=\"size-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin\" />\n ) : (\n <Save className=\"size-4\" />\n )}\n Create Promotion\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CreatePromotionPage.js","names":[],"sources":["../../../../../src/billing/modules/promotions/pages/CreatePromotionPage.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { ArrowLeft, Save, Gift, Info } from 'lucide-react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { useCreatePromotionMutation } from '../../../../generated/global-operations';\n\nconst KNOWN_PRODUCTS = ['vibecontrols', 'workspaces', 'botlit', 'fluidgrids', 'healthybowl'];\n\ninterface FormData {\n name: string;\n description: string;\n startsAt: string;\n expiresAt: string;\n bonusMonths: number;\n maxRedemptions: string;\n applicableProductIds: string[];\n applicablePlanIds: string;\n}\n\nexport const CreatePromotionPage: FC = () => {\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const [createPromotion] = useCreatePromotionMutation({ refetchQueries: ['ListPromotions'] });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});\n\n const today = new Date().toISOString().split('T')[0];\n const [form, setForm] = useState<FormData>({\n name: '',\n description: '',\n startsAt: today,\n expiresAt: '',\n bonusMonths: 1,\n maxRedemptions: '',\n applicableProductIds: [],\n applicablePlanIds: '',\n });\n\n if (!permissions.canManagePlans) {\n return <AccessDenied message=\"You don't have permission to create promotions.\" />;\n }\n\n const set = <K extends keyof FormData>(k: K, v: FormData[K]) =>\n setForm((prev) => ({ ...prev, [k]: v }));\n\n const toggleProduct = (pid: string) => {\n set(\n 'applicableProductIds',\n form.applicableProductIds.includes(pid)\n ? form.applicableProductIds.filter((p) => p !== pid)\n : [...form.applicableProductIds, pid]\n );\n };\n\n const validate = () => {\n const e: typeof errors = {};\n if (!form.name.trim()) e.name = 'Name is required';\n if (!form.startsAt) e.startsAt = 'Start date is required';\n if (!form.expiresAt) e.expiresAt = 'End date is required';\n if (form.expiresAt && form.expiresAt <= form.startsAt)\n e.expiresAt = 'End date must be after start date';\n if (form.bonusMonths < 1) e.bonusMonths = 'Must be at least 1';\n setErrors(e);\n return Object.keys(e).length === 0;\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n setIsSubmitting(true);\n try {\n const planIds = form.applicablePlanIds.split(',').flatMap((s) => {\n const trimmed = s.trim();\n return trimmed ? [trimmed] : [];\n });\n\n await createPromotion({\n variables: {\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n startsAt: new Date(form.startsAt).toISOString(),\n expiresAt: new Date(form.expiresAt).toISOString(),\n bonusMonths: form.bonusMonths,\n maxRedemptions: form.maxRedemptions ? parseInt(form.maxRedemptions) : undefined,\n applicableProductIds: form.applicableProductIds,\n applicablePlanIds: planIds,\n },\n },\n });\n navigateTo('/promotions');\n } catch (err) {\n setErrors({ name: err instanceof Error ? err.message : 'Failed to create promotion' });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div className=\"max-w-2xl mx-auto p-4 sm:p-6 space-y-6\">\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/promotions')}\n aria-label=\"Back to promotions\"\n className=\"p-2 rounded-button hover:bg-bg-sunken text-text-muted hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-5\" />\n </button>\n <PageHeader\n title=\"Create Promotion\"\n description=\"Offer bonus free renewal months to subscribers who sign up within the promotion window\"\n icon={<Gift className=\"size-6\" />}\n />\n </div>\n\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Basic info */}\n <div className=\"bg-bg-surface border border-border-subtle rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Promotion Details</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">Name *</label>\n <input\n type=\"text\"\n value={form.name}\n onChange={(e) => set('name', e.target.value)}\n placeholder=\"e.g. June Launch Offer\"\n className={`w-full px-3 py-2 rounded-input border ${errors.name ? 'border-destructive' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.name && <p className=\"text-xs text-status-error-text mt-1\">{errors.name}</p>}\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">Description</label>\n <textarea\n value={form.description}\n onChange={(e) => set('description', e.target.value)}\n placeholder=\"Subscribe in June and get 2 extra months free!\"\n rows={2}\n className=\"w-full px-3 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </div>\n\n {/* Bonus config */}\n <div className=\"bg-bg-surface border border-border-subtle rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Bonus Configuration</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Bonus Months *\n </label>\n <input\n type=\"number\"\n value={form.bonusMonths}\n min={1}\n max={12}\n onChange={(e) => set('bonusMonths', parseInt(e.target.value) || 1)}\n className={`w-32 px-3 py-2 rounded-input border ${errors.bonusMonths ? 'border-destructive' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.bonusMonths && (\n <p className=\"text-xs text-status-error-text mt-1\">{errors.bonusMonths}</p>\n )}\n <div className=\"mt-2 flex items-start gap-2 p-3 bg-[var(--color-accent-soft)] rounded-lg text-sm text-text-muted\">\n <Info className=\"size-4 shrink-0 mt-0.5 text-text-link\" />\n <span>\n Users who subscribe during this window get{' '}\n <strong className=\"text-text-primary\">\n {form.bonusMonths} free renewal{form.bonusMonths !== 1 ? 's' : ''}\n </strong>{' '}\n after their first paid period — their quota resets each month as normal.\n </span>\n </div>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Max Redemptions\n </label>\n <input\n type=\"number\"\n value={form.maxRedemptions}\n min={1}\n onChange={(e) => set('maxRedemptions', e.target.value)}\n placeholder=\"Unlimited\"\n className=\"w-48 px-3 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <p className=\"text-xs text-text-muted mt-1\">Leave empty for no cap</p>\n </div>\n </div>\n\n {/* Date window */}\n <div className=\"bg-bg-surface border border-border-subtle rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Active Window</h3>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Start Date *\n </label>\n <input\n type=\"date\"\n value={form.startsAt}\n onChange={(e) => set('startsAt', e.target.value)}\n className={`w-full px-3 py-2 rounded-input border ${errors.startsAt ? 'border-destructive' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.startsAt && (\n <p className=\"text-xs text-status-error-text mt-1\">{errors.startsAt}</p>\n )}\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">End Date *</label>\n <input\n type=\"date\"\n value={form.expiresAt}\n onChange={(e) => set('expiresAt', e.target.value)}\n min={form.startsAt}\n className={`w-full px-3 py-2 rounded-input border ${errors.expiresAt ? 'border-destructive' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.expiresAt && (\n <p className=\"text-xs text-status-error-text mt-1\">{errors.expiresAt}</p>\n )}\n </div>\n </div>\n <p className=\"text-xs text-text-muted\">\n Only purchases made between these dates will receive the bonus months\n </p>\n </div>\n\n {/* Scope */}\n <div className=\"bg-bg-surface border border-border-subtle rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Scope</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-2\">\n Applicable Products\n </label>\n <div className=\"flex flex-wrap gap-2\">\n {KNOWN_PRODUCTS.map((pid) => (\n <button\n key={pid}\n type=\"button\"\n onClick={() => toggleProduct(pid)}\n className={`px-3 py-1.5 rounded-lg border text-sm transition-colors ${\n form.applicableProductIds.includes(pid)\n ? 'border-action-primary-bg bg-[var(--color-accent-soft)] text-text-link font-medium'\n : 'border-border-subtle hover:border-action-primary-bg/50'\n }`}\n >\n {pid}\n </button>\n ))}\n </div>\n <p className=\"text-xs text-text-muted mt-1\">\n {form.applicableProductIds.length === 0\n ? 'No selection = applies to all products'\n : `Applies to: ${form.applicableProductIds.join(', ')}`}\n </p>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Plan IDs (optional)\n </label>\n <input\n type=\"text\"\n value={form.applicablePlanIds}\n onChange={(e) => set('applicablePlanIds', e.target.value)}\n placeholder=\"plan-vibecontrols-pro-monthly, plan-vibecontrols-pro-yearly\"\n className=\"w-full px-3 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary placeholder:text-text-muted text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <p className=\"text-xs text-text-muted mt-1\">\n Comma-separated. Leave empty to apply to all plans\n </p>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex gap-3 justify-end\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/promotions')}\n className=\"px-5 py-2 rounded-button border border-border-subtle text-text-primary hover:bg-bg-sunken text-sm font-medium transition-colors\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting}\n className=\"px-5 py-2 rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover flex items-center gap-2 text-sm font-medium transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-50\"\n >\n {isSubmitting ? (\n <div className=\"size-4 border-2 border-action-primary-text border-t-transparent rounded-full animate-spin\" />\n ) : (\n <Save className=\"size-4\" />\n )}\n Create Promotion\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -23,16 +23,16 @@ var _ = () => {
|
|
|
23
23
|
}
|
|
24
24
|
}, A = /* @__PURE__ */ new Date(), j = (e) => e.isActive ? new Date(e.expiresAt) < A ? {
|
|
25
25
|
text: "Expired",
|
|
26
|
-
cls: "bg-
|
|
26
|
+
cls: "bg-status-error-bg-subtle text-status-error-text"
|
|
27
27
|
} : new Date(e.startsAt) > A ? {
|
|
28
28
|
text: "Upcoming",
|
|
29
|
-
cls: "bg-
|
|
29
|
+
cls: "bg-[var(--color-accent-soft)] text-text-link"
|
|
30
30
|
} : {
|
|
31
31
|
text: "Live",
|
|
32
|
-
cls: "bg-
|
|
32
|
+
cls: "bg-status-success-bg-subtle text-status-success-text"
|
|
33
33
|
} : {
|
|
34
34
|
text: "Inactive",
|
|
35
|
-
cls: "bg-
|
|
35
|
+
cls: "bg-bg-sunken text-text-muted"
|
|
36
36
|
};
|
|
37
37
|
return /* @__PURE__ */ g("div", {
|
|
38
38
|
className: "max-w-5xl mx-auto p-4 sm:p-6 space-y-6",
|
|
@@ -45,40 +45,43 @@ var _ = () => {
|
|
|
45
45
|
icon: /* @__PURE__ */ h(l, { className: "size-6" })
|
|
46
46
|
}), /* @__PURE__ */ g("button", {
|
|
47
47
|
onClick: () => _("/promotions/create"),
|
|
48
|
-
className: "flex items-center gap-2 px-4 py-2 rounded-
|
|
48
|
+
className: "flex items-center gap-2 px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover text-sm font-medium shrink-0 transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
49
49
|
children: [/* @__PURE__ */ h(u, { className: "size-4" }), "New Promotion"]
|
|
50
50
|
})]
|
|
51
51
|
}),
|
|
52
52
|
/* @__PURE__ */ h("div", {
|
|
53
53
|
className: "flex items-center gap-3",
|
|
54
54
|
children: /* @__PURE__ */ g("label", {
|
|
55
|
-
className: "flex items-center gap-2 text-sm text-muted
|
|
55
|
+
className: "flex items-center gap-2 text-sm text-text-muted cursor-pointer select-none",
|
|
56
56
|
children: [/* @__PURE__ */ h("input", {
|
|
57
57
|
type: "checkbox",
|
|
58
58
|
checked: y,
|
|
59
59
|
onChange: (e) => b(e.target.checked),
|
|
60
|
-
className: "rounded border-border"
|
|
60
|
+
className: "rounded border-border-subtle"
|
|
61
61
|
}), "Show inactive / expired"]
|
|
62
62
|
})
|
|
63
63
|
}),
|
|
64
64
|
w ? /* @__PURE__ */ h("div", {
|
|
65
|
-
className: "text-center py-16 text-muted
|
|
65
|
+
className: "text-center py-16 text-text-muted",
|
|
66
66
|
children: "Loading promotions…"
|
|
67
67
|
}) : O.length === 0 ? /* @__PURE__ */ g("div", {
|
|
68
|
-
className: "text-center py-16 border border-dashed border-border rounded-
|
|
68
|
+
className: "flex flex-col items-center justify-center text-center py-16 border border-dashed border-border-subtle rounded-card bg-bg-surface",
|
|
69
69
|
children: [
|
|
70
|
-
/* @__PURE__ */ h(
|
|
70
|
+
/* @__PURE__ */ h("div", {
|
|
71
|
+
className: "flex size-12 items-center justify-center rounded-full bg-[var(--color-accent-soft)] mb-4",
|
|
72
|
+
children: /* @__PURE__ */ h(l, { className: "size-6 text-text-link" })
|
|
73
|
+
}),
|
|
71
74
|
/* @__PURE__ */ h("p", {
|
|
72
|
-
className: "text-
|
|
75
|
+
className: "text-base font-semibold text-text-primary",
|
|
73
76
|
children: "No promotions yet"
|
|
74
77
|
}),
|
|
75
78
|
/* @__PURE__ */ h("p", {
|
|
76
|
-
className: "text-sm text-muted
|
|
79
|
+
className: "text-sm text-text-muted mt-1",
|
|
77
80
|
children: "Create one to offer bonus months to early subscribers"
|
|
78
81
|
}),
|
|
79
82
|
/* @__PURE__ */ h("button", {
|
|
80
83
|
onClick: () => _("/promotions/create"),
|
|
81
|
-
className: "mt-4 px-4 py-2 rounded-
|
|
84
|
+
className: "mt-4 px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text text-sm font-medium transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
82
85
|
children: "Create Promotion"
|
|
83
86
|
})
|
|
84
87
|
]
|
|
@@ -87,14 +90,14 @@ var _ = () => {
|
|
|
87
90
|
children: O.map((e) => {
|
|
88
91
|
let { text: t, cls: n } = j(e), r = x === e.id;
|
|
89
92
|
return /* @__PURE__ */ g("div", {
|
|
90
|
-
className: "bg-
|
|
93
|
+
className: "bg-bg-surface border border-border-subtle rounded-card p-5 flex flex-col sm:flex-row sm:items-center gap-4 shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)]",
|
|
91
94
|
children: [/* @__PURE__ */ g("div", {
|
|
92
95
|
className: "flex-1 min-w-0",
|
|
93
96
|
children: [
|
|
94
97
|
/* @__PURE__ */ g("div", {
|
|
95
98
|
className: "flex items-center gap-2 mb-1",
|
|
96
99
|
children: [/* @__PURE__ */ h("span", {
|
|
97
|
-
className: "font-semibold text-
|
|
100
|
+
className: "font-semibold text-text-primary truncate",
|
|
98
101
|
children: e.name
|
|
99
102
|
}), /* @__PURE__ */ h("span", {
|
|
100
103
|
className: `text-xs font-medium px-2 py-0.5 rounded-full ${n}`,
|
|
@@ -102,18 +105,18 @@ var _ = () => {
|
|
|
102
105
|
})]
|
|
103
106
|
}),
|
|
104
107
|
e.description && /* @__PURE__ */ h("p", {
|
|
105
|
-
className: "text-sm text-muted
|
|
108
|
+
className: "text-sm text-text-muted mb-2 line-clamp-1",
|
|
106
109
|
children: e.description
|
|
107
110
|
}),
|
|
108
111
|
/* @__PURE__ */ g("div", {
|
|
109
|
-
className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted
|
|
112
|
+
className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-text-muted",
|
|
110
113
|
children: [
|
|
111
114
|
/* @__PURE__ */ g("span", {
|
|
112
115
|
className: "flex items-center gap-1",
|
|
113
116
|
children: [
|
|
114
117
|
/* @__PURE__ */ h(l, { className: "size-3" }),
|
|
115
118
|
/* @__PURE__ */ h("strong", {
|
|
116
|
-
className: "text-
|
|
119
|
+
className: "text-text-primary",
|
|
117
120
|
children: e.bonusMonths
|
|
118
121
|
}),
|
|
119
122
|
" bonus month",
|
|
@@ -152,7 +155,7 @@ var _ = () => {
|
|
|
152
155
|
onClick: () => k(e.id, e.isActive),
|
|
153
156
|
disabled: r,
|
|
154
157
|
title: e.isActive ? "Deactivate" : "Reactivate",
|
|
155
|
-
className: `p-2 rounded-
|
|
158
|
+
className: `p-2 rounded-button transition-colors ${e.isActive ? "text-status-success-text hover:bg-status-success-bg-subtle" : "text-text-muted hover:bg-bg-sunken"} disabled:opacity-50`,
|
|
156
159
|
children: e.isActive ? /* @__PURE__ */ h(p, { className: "size-5" }) : /* @__PURE__ */ h(f, { className: "size-5" })
|
|
157
160
|
})
|
|
158
161
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PromotionsListPage.js","names":[],"sources":["../../../../../src/billing/modules/promotions/pages/PromotionsListPage.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { Plus, Tag, Calendar, Users, ToggleLeft, ToggleRight, Gift } from 'lucide-react';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport {\n useListPromotionsQuery,\n useDeactivatePromotionMutation,\n useUpdatePromotionMutation,\n} from '../../../../generated/global-operations';\n\nexport const PromotionsListPage: FC = () => {\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const [showInactive, setShowInactive] = useState(false);\n const [actionId, setActionId] = useState<string | null>(null);\n\n const { data, loading, refetch } = useListPromotionsQuery({\n variables: { includeInactive: showInactive },\n });\n\n const [deactivatePromotion] = useDeactivatePromotionMutation();\n const [updatePromotion] = useUpdatePromotionMutation();\n\n if (!permissions.canManagePlans) {\n return <AccessDenied message=\"You don't have permission to manage promotions.\" />;\n }\n\n const promotions = data?.listPromotions ?? [];\n\n const handleToggle = async (id: string, currentlyActive: boolean) => {\n setActionId(id);\n try {\n if (currentlyActive) {\n await deactivatePromotion({ variables: { id } });\n } else {\n await updatePromotion({ variables: { id, input: { isActive: true } } });\n }\n await refetch();\n } finally {\n setActionId(null);\n }\n };\n\n const now = new Date();\n const statusLabel = (p: { isActive: boolean; startsAt: string; expiresAt: string }) => {\n if (!p.isActive) return { text: 'Inactive', cls: 'bg-muted text-muted-foreground' };\n if (new Date(p.expiresAt) < now)\n return { text: 'Expired', cls: 'bg-destructive/10 text-destructive' };\n if (new Date(p.startsAt) > now) return { text: 'Upcoming', cls: 'bg-primary/10 text-primary' };\n return { text: 'Live', cls: 'bg-green-500/10 text-green-600' };\n };\n\n return (\n <div className=\"max-w-5xl mx-auto p-4 sm:p-6 space-y-6\">\n <div className=\"flex items-center justify-between gap-4\">\n <PageHeader\n title=\"Promotions\"\n description=\"Time-bounded offers that grant bonus free renewal months to subscribers\"\n icon={<Gift className=\"size-6\" />}\n />\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 text-sm font-medium shrink-0\"\n >\n <Plus className=\"size-4\" />\n New Promotion\n </button>\n </div>\n\n {/* Filter */}\n <div className=\"flex items-center gap-3\">\n <label className=\"flex items-center gap-2 text-sm text-muted-foreground cursor-pointer select-none\">\n <input\n type=\"checkbox\"\n checked={showInactive}\n onChange={(e) => setShowInactive(e.target.checked)}\n className=\"rounded border-border\"\n />\n Show inactive / expired\n </label>\n </div>\n\n {loading ? (\n <div className=\"text-center py-16 text-muted-foreground\">Loading promotions…</div>\n ) : promotions.length === 0 ? (\n <div className=\"text-center py-16 border border-dashed border-border rounded-xl\">\n <Gift className=\"size-10 mx-auto mb-3 text-muted-foreground/50\" />\n <p className=\"text-muted-foreground font-medium\">No promotions yet</p>\n <p className=\"text-sm text-muted-foreground mt-1\">\n Create one to offer bonus months to early subscribers\n </p>\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"mt-4 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm\"\n >\n Create Promotion\n </button>\n </div>\n ) : (\n <div className=\"space-y-3\">\n {promotions.map((p: (typeof promotions)[number]) => {\n const { text, cls } = statusLabel(p);\n const busy = actionId === p.id;\n return (\n <div\n key={p.id}\n className=\"bg-card border border-border rounded-xl p-5 flex flex-col sm:flex-row sm:items-center gap-4\"\n >\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 mb-1\">\n <span className=\"font-semibold text-foreground truncate\">{p.name}</span>\n <span className={`text-xs font-medium px-2 py-0.5 rounded-full ${cls}`}>\n {text}\n </span>\n </div>\n {p.description && (\n <p className=\"text-sm text-muted-foreground mb-2 line-clamp-1\">\n {p.description}\n </p>\n )}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground\">\n <span className=\"flex items-center gap-1\">\n <Gift className=\"size-3\" />\n <strong className=\"text-foreground\">{p.bonusMonths}</strong> bonus month\n {p.bonusMonths !== 1 ? 's' : ''}\n </span>\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"size-3\" />\n {new Date(p.startsAt).toLocaleDateString()} –{' '}\n {new Date(p.expiresAt).toLocaleDateString()}\n </span>\n <span className=\"flex items-center gap-1\">\n <Users className=\"size-3\" />\n {p.currentRedemptions}\n {p.maxRedemptions ? ` / ${p.maxRedemptions}` : ''} redemptions\n </span>\n {p.applicableProductIds.length > 0 && (\n <span className=\"flex items-center gap-1\">\n <Tag className=\"size-3\" />\n {p.applicableProductIds.join(', ')}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"flex items-center gap-2 shrink-0\">\n <button\n onClick={() => handleToggle(p.id, p.isActive)}\n disabled={busy}\n title={p.isActive ? 'Deactivate' : 'Reactivate'}\n className={`p-2 rounded-lg transition-colors ${\n p.isActive\n ? 'text-green-600 hover:bg-green-50'\n : 'text-muted-foreground hover:bg-muted'\n } disabled:opacity-50`}\n >\n {p.isActive ? (\n <ToggleRight className=\"size-5\" />\n ) : (\n <ToggleLeft className=\"size-5\" />\n )}\n </button>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PromotionsListPage.js","names":[],"sources":["../../../../../src/billing/modules/promotions/pages/PromotionsListPage.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { Plus, Tag, Calendar, Users, ToggleLeft, ToggleRight, Gift } from 'lucide-react';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport {\n useListPromotionsQuery,\n useDeactivatePromotionMutation,\n useUpdatePromotionMutation,\n} from '../../../../generated/global-operations';\n\nexport const PromotionsListPage: FC = () => {\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const [showInactive, setShowInactive] = useState(false);\n const [actionId, setActionId] = useState<string | null>(null);\n\n const { data, loading, refetch } = useListPromotionsQuery({\n variables: { includeInactive: showInactive },\n });\n\n const [deactivatePromotion] = useDeactivatePromotionMutation();\n const [updatePromotion] = useUpdatePromotionMutation();\n\n if (!permissions.canManagePlans) {\n return <AccessDenied message=\"You don't have permission to manage promotions.\" />;\n }\n\n const promotions = data?.listPromotions ?? [];\n\n const handleToggle = async (id: string, currentlyActive: boolean) => {\n setActionId(id);\n try {\n if (currentlyActive) {\n await deactivatePromotion({ variables: { id } });\n } else {\n await updatePromotion({ variables: { id, input: { isActive: true } } });\n }\n await refetch();\n } finally {\n setActionId(null);\n }\n };\n\n const now = new Date();\n const statusLabel = (p: { isActive: boolean; startsAt: string; expiresAt: string }) => {\n if (!p.isActive) return { text: 'Inactive', cls: 'bg-bg-sunken text-text-muted' };\n if (new Date(p.expiresAt) < now)\n return { text: 'Expired', cls: 'bg-status-error-bg-subtle text-status-error-text' };\n if (new Date(p.startsAt) > now)\n return { text: 'Upcoming', cls: 'bg-[var(--color-accent-soft)] text-text-link' };\n return { text: 'Live', cls: 'bg-status-success-bg-subtle text-status-success-text' };\n };\n\n return (\n <div className=\"max-w-5xl mx-auto p-4 sm:p-6 space-y-6\">\n <div className=\"flex items-center justify-between gap-4\">\n <PageHeader\n title=\"Promotions\"\n description=\"Time-bounded offers that grant bonus free renewal months to subscribers\"\n icon={<Gift className=\"size-6\" />}\n />\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"flex items-center gap-2 px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover text-sm font-medium shrink-0 transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Plus className=\"size-4\" />\n New Promotion\n </button>\n </div>\n\n {/* Filter */}\n <div className=\"flex items-center gap-3\">\n <label className=\"flex items-center gap-2 text-sm text-text-muted cursor-pointer select-none\">\n <input\n type=\"checkbox\"\n checked={showInactive}\n onChange={(e) => setShowInactive(e.target.checked)}\n className=\"rounded border-border-subtle\"\n />\n Show inactive / expired\n </label>\n </div>\n\n {loading ? (\n <div className=\"text-center py-16 text-text-muted\">Loading promotions…</div>\n ) : promotions.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center text-center py-16 border border-dashed border-border-subtle rounded-card bg-bg-surface\">\n <div className=\"flex size-12 items-center justify-center rounded-full bg-[var(--color-accent-soft)] mb-4\">\n <Gift className=\"size-6 text-text-link\" />\n </div>\n <p className=\"text-base font-semibold text-text-primary\">No promotions yet</p>\n <p className=\"text-sm text-text-muted mt-1\">\n Create one to offer bonus months to early subscribers\n </p>\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"mt-4 px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text text-sm font-medium transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n Create Promotion\n </button>\n </div>\n ) : (\n <div className=\"space-y-3\">\n {promotions.map((p: (typeof promotions)[number]) => {\n const { text, cls } = statusLabel(p);\n const busy = actionId === p.id;\n return (\n <div\n key={p.id}\n className=\"bg-bg-surface border border-border-subtle rounded-card p-5 flex flex-col sm:flex-row sm:items-center gap-4 shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)]\"\n >\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 mb-1\">\n <span className=\"font-semibold text-text-primary truncate\">{p.name}</span>\n <span className={`text-xs font-medium px-2 py-0.5 rounded-full ${cls}`}>\n {text}\n </span>\n </div>\n {p.description && (\n <p className=\"text-sm text-text-muted mb-2 line-clamp-1\">{p.description}</p>\n )}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 text-xs text-text-muted\">\n <span className=\"flex items-center gap-1\">\n <Gift className=\"size-3\" />\n <strong className=\"text-text-primary\">{p.bonusMonths}</strong> bonus month\n {p.bonusMonths !== 1 ? 's' : ''}\n </span>\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"size-3\" />\n {new Date(p.startsAt).toLocaleDateString()} –{' '}\n {new Date(p.expiresAt).toLocaleDateString()}\n </span>\n <span className=\"flex items-center gap-1\">\n <Users className=\"size-3\" />\n {p.currentRedemptions}\n {p.maxRedemptions ? ` / ${p.maxRedemptions}` : ''} redemptions\n </span>\n {p.applicableProductIds.length > 0 && (\n <span className=\"flex items-center gap-1\">\n <Tag className=\"size-3\" />\n {p.applicableProductIds.join(', ')}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"flex items-center gap-2 shrink-0\">\n <button\n onClick={() => handleToggle(p.id, p.isActive)}\n disabled={busy}\n title={p.isActive ? 'Deactivate' : 'Reactivate'}\n className={`p-2 rounded-button transition-colors ${\n p.isActive\n ? 'text-status-success-text hover:bg-status-success-bg-subtle'\n : 'text-text-muted hover:bg-bg-sunken'\n } disabled:opacity-50`}\n >\n {p.isActive ? (\n <ToggleRight className=\"size-5\" />\n ) : (\n <ToggleLeft className=\"size-5\" />\n )}\n </button>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|