@burdenoff/microfe-billing 2026.803.1 → 2026.805.1
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/modules/plans/pages/PlanBrowseDetailPage.js +97 -97
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanDetailPage.js +108 -108
- package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
- package/dist/billing/shared/utils/planFeatureQuantity.js +21 -1
- package/dist/billing/shared/utils/planFeatureQuantity.js.map +1 -1
- package/dist/generated/global-operations.js +1 -0
- package/dist/generated/global-operations.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,29 +2,29 @@ import { ServerError as e } from "../../../shared/components/ServerError.js";
|
|
|
2
2
|
import "../../../shared/components/index.js";
|
|
3
3
|
import { formatCurrency as t, formatPlanDuration as n, formatPlanDurationLabel as r, formatQuotaValue as i } from "../../../shared/utils/format.js";
|
|
4
4
|
import { isServerError as a } from "../../../shared/utils/error.js";
|
|
5
|
-
import { getPlanFeatureQuantity as o, getPlanFeatureUnit as s } from "../../../shared/utils/planFeatureQuantity.js";
|
|
5
|
+
import { getPlanFeatureQuantity as o, getPlanFeatureUnit as s, getQuotaDisplayName as c } from "../../../shared/utils/planFeatureQuantity.js";
|
|
6
6
|
import "../../../shared/utils/index.js";
|
|
7
|
-
import { useBillingNavigate as
|
|
8
|
-
import { usePlan as
|
|
7
|
+
import { useBillingNavigate as l } from "../../../hooks/useBillingNavigate.js";
|
|
8
|
+
import { usePlan as u } from "../hooks/usePlans.js";
|
|
9
9
|
import "../hooks/index.js";
|
|
10
|
-
import { useMemo as
|
|
11
|
-
import { useParams as
|
|
12
|
-
import { Fragment as
|
|
13
|
-
import { useI18n as
|
|
14
|
-
import { EmphasisPanel as
|
|
10
|
+
import { useMemo as d } from "react";
|
|
11
|
+
import { useParams as f } from "react-router";
|
|
12
|
+
import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
13
|
+
import { useI18n as g } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
14
|
+
import { EmphasisPanel as _, PagePurpose as v, ShowMore as y } from "@burdenoff/fe-libs/ui";
|
|
15
15
|
//#region src/billing/modules/plans/pages/PlanBrowseDetailPage.tsx
|
|
16
|
-
var
|
|
16
|
+
var b = ({ when: e, collapsedHeight: t, children: n }) => e ? /* @__PURE__ */ m(y, {
|
|
17
17
|
collapsedHeight: t,
|
|
18
18
|
children: n
|
|
19
|
-
}) : /* @__PURE__ */ p
|
|
20
|
-
function
|
|
19
|
+
}) : /* @__PURE__ */ m(p, { children: n });
|
|
20
|
+
function x(e) {
|
|
21
21
|
let t = /* @__PURE__ */ new Map();
|
|
22
22
|
for (let n of e) {
|
|
23
23
|
if (!n.quota) continue;
|
|
24
24
|
let e = n.quota.id, r = n.quota.limits?.type ?? "unknown", i = o(n), a = t.get(e);
|
|
25
25
|
a ? a.totalValue += i : t.set(e, {
|
|
26
26
|
quotaId: e,
|
|
27
|
-
name: n
|
|
27
|
+
name: c(n),
|
|
28
28
|
quotaType: n.quota.quotaType ?? "",
|
|
29
29
|
limitType: r,
|
|
30
30
|
totalValue: i,
|
|
@@ -33,116 +33,116 @@ function b(e) {
|
|
|
33
33
|
}
|
|
34
34
|
return Array.from(t.values());
|
|
35
35
|
}
|
|
36
|
-
var
|
|
37
|
-
let { t: o } =
|
|
36
|
+
var S = () => {
|
|
37
|
+
let { t: o } = g(), s = (e, t) => {
|
|
38
38
|
let n = o(e);
|
|
39
39
|
return n === e ? t : n;
|
|
40
|
-
}, { planId:
|
|
41
|
-
return S ? /* @__PURE__ */
|
|
40
|
+
}, { planId: c } = f(), p = l(), { plan: y, isLoading: S, error: C, refetch: w } = u(c), T = d(() => x(y?.features || []), [y?.features]);
|
|
41
|
+
return S ? /* @__PURE__ */ h("div", {
|
|
42
42
|
className: "space-y-6 px-6 lg:px-8 py-6",
|
|
43
|
-
children: [/* @__PURE__ */
|
|
43
|
+
children: [/* @__PURE__ */ h("div", {
|
|
44
44
|
className: "flex items-center gap-4",
|
|
45
|
-
children: [/* @__PURE__ */
|
|
46
|
-
}), /* @__PURE__ */
|
|
45
|
+
children: [/* @__PURE__ */ m("div", { className: "size-10 bg-bg-sunken animate-pulse rounded" }), /* @__PURE__ */ m("div", { className: "h-8 w-48 bg-bg-sunken animate-pulse rounded" })]
|
|
46
|
+
}), /* @__PURE__ */ h("div", {
|
|
47
47
|
className: "grid grid-cols-1 lg:grid-cols-3 gap-6",
|
|
48
|
-
children: [/* @__PURE__ */
|
|
48
|
+
children: [/* @__PURE__ */ m("div", {
|
|
49
49
|
className: "lg:col-span-2 space-y-6",
|
|
50
|
-
children: /* @__PURE__ */
|
|
50
|
+
children: /* @__PURE__ */ h("div", {
|
|
51
51
|
className: "border border-border-subtle rounded-lg p-6 space-y-4",
|
|
52
|
-
children: [/* @__PURE__ */
|
|
52
|
+
children: [/* @__PURE__ */ m("div", { className: "h-6 w-32 bg-bg-sunken animate-pulse rounded" }), /* @__PURE__ */ m("div", { className: "h-4 w-full bg-bg-sunken animate-pulse rounded" })]
|
|
53
53
|
})
|
|
54
|
-
}), /* @__PURE__ */
|
|
54
|
+
}), /* @__PURE__ */ h("div", {
|
|
55
55
|
className: "border border-border-subtle rounded-lg p-6 space-y-4",
|
|
56
|
-
children: [/* @__PURE__ */
|
|
56
|
+
children: [/* @__PURE__ */ m("div", { className: "h-6 w-24 bg-bg-sunken animate-pulse rounded" }), /* @__PURE__ */ m("div", { className: "h-10 w-full bg-bg-sunken animate-pulse rounded" })]
|
|
57
57
|
})]
|
|
58
58
|
})]
|
|
59
|
-
}) : C && a(C) ? /* @__PURE__ */
|
|
59
|
+
}) : C && a(C) ? /* @__PURE__ */ m("div", {
|
|
60
60
|
className: "p-6",
|
|
61
|
-
children: /* @__PURE__ */
|
|
61
|
+
children: /* @__PURE__ */ m(e, {
|
|
62
62
|
title: s("billing.plans.serverUnavailable", "Server Unavailable"),
|
|
63
63
|
message: s("billing.plans.unableToLoadPlanDetails", "Unable to load plan details."),
|
|
64
64
|
onRetry: () => w(),
|
|
65
65
|
showRetry: !0
|
|
66
66
|
})
|
|
67
|
-
}) :
|
|
67
|
+
}) : y ? /* @__PURE__ */ h("div", {
|
|
68
68
|
className: "space-y-6 px-6 lg:px-8 py-6",
|
|
69
|
-
children: [/* @__PURE__ */
|
|
69
|
+
children: [/* @__PURE__ */ h("div", {
|
|
70
70
|
className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
|
|
71
|
-
children: [/* @__PURE__ */
|
|
71
|
+
children: [/* @__PURE__ */ h("div", {
|
|
72
72
|
className: "flex items-start gap-4",
|
|
73
|
-
children: [/* @__PURE__ */
|
|
73
|
+
children: [/* @__PURE__ */ m("button", {
|
|
74
74
|
type: "button",
|
|
75
|
-
onClick: () =>
|
|
75
|
+
onClick: () => p("/plans"),
|
|
76
76
|
className: "p-2 -ml-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors",
|
|
77
|
-
children: /* @__PURE__ */
|
|
77
|
+
children: /* @__PURE__ */ m("svg", {
|
|
78
78
|
className: "size-5",
|
|
79
79
|
fill: "none",
|
|
80
80
|
viewBox: "0 0 24 24",
|
|
81
81
|
stroke: "currentColor",
|
|
82
|
-
children: /* @__PURE__ */
|
|
82
|
+
children: /* @__PURE__ */ m("path", {
|
|
83
83
|
strokeLinecap: "round",
|
|
84
84
|
strokeLinejoin: "round",
|
|
85
85
|
strokeWidth: 2,
|
|
86
86
|
d: "M15 19l-7-7 7-7"
|
|
87
87
|
})
|
|
88
88
|
})
|
|
89
|
-
}), /* @__PURE__ */
|
|
90
|
-
/* @__PURE__ */
|
|
89
|
+
}), /* @__PURE__ */ h("div", { children: [
|
|
90
|
+
/* @__PURE__ */ m("h1", {
|
|
91
91
|
className: "text-2xl md:text-3xl font-semibold tracking-tight text-text-primary",
|
|
92
|
-
children:
|
|
92
|
+
children: y.name
|
|
93
93
|
}),
|
|
94
|
-
/* @__PURE__ */
|
|
94
|
+
/* @__PURE__ */ h("p", {
|
|
95
95
|
className: "text-sm text-text-secondary mt-1",
|
|
96
|
-
children: [r(
|
|
96
|
+
children: [r(y.duration), " billing plan"]
|
|
97
97
|
}),
|
|
98
|
-
/* @__PURE__ */
|
|
98
|
+
/* @__PURE__ */ m(v, {
|
|
99
99
|
className: "mt-3",
|
|
100
100
|
children: s("billing.plans.browseDetailPurpose", "Everything this plan includes — its price, billing period and the full list of features and quotas — so you can confirm it fits before subscribing. Hit Subscribe when you are ready to check out.")
|
|
101
101
|
})
|
|
102
102
|
] })]
|
|
103
|
-
}), /* @__PURE__ */
|
|
103
|
+
}), /* @__PURE__ */ m("button", {
|
|
104
104
|
type: "button",
|
|
105
|
-
onClick: () =>
|
|
105
|
+
onClick: () => p(`/checkout/${y.id}`),
|
|
106
106
|
className: "px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors",
|
|
107
107
|
children: "Subscribe to Plan"
|
|
108
108
|
})]
|
|
109
|
-
}), /* @__PURE__ */
|
|
109
|
+
}), /* @__PURE__ */ h("div", {
|
|
110
110
|
className: "grid grid-cols-1 lg:grid-cols-3 gap-6",
|
|
111
|
-
children: [/* @__PURE__ */
|
|
111
|
+
children: [/* @__PURE__ */ h("div", {
|
|
112
112
|
className: "lg:col-span-2 space-y-6",
|
|
113
|
-
children: [/* @__PURE__ */
|
|
113
|
+
children: [/* @__PURE__ */ h(_, {
|
|
114
114
|
className: "overflow-hidden p-0",
|
|
115
|
-
children: [/* @__PURE__ */
|
|
115
|
+
children: [/* @__PURE__ */ m("div", {
|
|
116
116
|
className: "p-6 border-b border-border-seam",
|
|
117
|
-
children: /* @__PURE__ */
|
|
117
|
+
children: /* @__PURE__ */ m("h2", {
|
|
118
118
|
className: "text-lg font-semibold text-text-primary",
|
|
119
119
|
children: s("billing.plans.pricing", "Pricing")
|
|
120
120
|
})
|
|
121
|
-
}), /* @__PURE__ */
|
|
121
|
+
}), /* @__PURE__ */ h("div", {
|
|
122
122
|
className: "p-6",
|
|
123
|
-
children: [/* @__PURE__ */
|
|
123
|
+
children: [/* @__PURE__ */ h("div", {
|
|
124
124
|
className: "flex items-baseline gap-1",
|
|
125
|
-
children: [/* @__PURE__ */
|
|
125
|
+
children: [/* @__PURE__ */ m("span", {
|
|
126
126
|
className: "text-4xl font-bold text-text-primary",
|
|
127
|
-
children: t(
|
|
128
|
-
}), /* @__PURE__ */
|
|
127
|
+
children: t(y.price, y.currency)
|
|
128
|
+
}), /* @__PURE__ */ h("span", {
|
|
129
129
|
className: "text-lg text-text-secondary",
|
|
130
|
-
children: ["/ ", n(
|
|
130
|
+
children: ["/ ", n(y.duration)]
|
|
131
131
|
})]
|
|
132
|
-
}), /* @__PURE__ */
|
|
132
|
+
}), /* @__PURE__ */ h("p", {
|
|
133
133
|
className: "text-sm text-text-secondary mt-2",
|
|
134
134
|
children: [
|
|
135
135
|
s("billing.plans.billed", "Billed"),
|
|
136
136
|
" ",
|
|
137
|
-
|
|
137
|
+
y.duration === "monthly" ? s("billing.plans.everyMonth", "every month") : s("billing.plans.annually", "annually")
|
|
138
138
|
]
|
|
139
139
|
})]
|
|
140
140
|
})]
|
|
141
|
-
}), /* @__PURE__ */
|
|
141
|
+
}), /* @__PURE__ */ h("div", {
|
|
142
142
|
className: "border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden",
|
|
143
|
-
children: [/* @__PURE__ */
|
|
143
|
+
children: [/* @__PURE__ */ m("div", {
|
|
144
144
|
className: "p-6 border-b border-border-seam bg-bg-sunken",
|
|
145
|
-
children: /* @__PURE__ */
|
|
145
|
+
children: /* @__PURE__ */ h("h2", {
|
|
146
146
|
className: "text-lg font-semibold text-text-primary",
|
|
147
147
|
children: [
|
|
148
148
|
s("billing.plans.featuresAndQuotas", "Features & Quotas"),
|
|
@@ -151,49 +151,49 @@ var x = () => {
|
|
|
151
151
|
")"
|
|
152
152
|
]
|
|
153
153
|
})
|
|
154
|
-
}), /* @__PURE__ */
|
|
154
|
+
}), /* @__PURE__ */ m("div", {
|
|
155
155
|
className: "p-6",
|
|
156
|
-
children: T.length === 0 ? /* @__PURE__ */
|
|
156
|
+
children: T.length === 0 ? /* @__PURE__ */ m("p", {
|
|
157
157
|
className: "text-sm text-text-secondary",
|
|
158
158
|
children: s("billing.plans.noFeatures", "No features configured for this plan.")
|
|
159
|
-
}) : /* @__PURE__ */
|
|
159
|
+
}) : /* @__PURE__ */ m(b, {
|
|
160
160
|
when: T.length > 4,
|
|
161
161
|
collapsedHeight: 280,
|
|
162
|
-
children: /* @__PURE__ */
|
|
162
|
+
children: /* @__PURE__ */ m("ul", {
|
|
163
163
|
className: "space-y-3",
|
|
164
|
-
children: T.map((e) => /* @__PURE__ */
|
|
164
|
+
children: T.map((e) => /* @__PURE__ */ h("li", {
|
|
165
165
|
className: "flex items-start gap-3 p-3 rounded-lg bg-bg-sunken",
|
|
166
|
-
children: [/* @__PURE__ */
|
|
166
|
+
children: [/* @__PURE__ */ m("div", {
|
|
167
167
|
className: "size-8 rounded-full bg-status-success-bg-subtle flex items-center justify-center shrink-0",
|
|
168
|
-
children: /* @__PURE__ */
|
|
168
|
+
children: /* @__PURE__ */ m("svg", {
|
|
169
169
|
className: "size-4 text-status-success-text",
|
|
170
170
|
fill: "none",
|
|
171
171
|
viewBox: "0 0 24 24",
|
|
172
172
|
stroke: "currentColor",
|
|
173
|
-
children: /* @__PURE__ */
|
|
173
|
+
children: /* @__PURE__ */ m("path", {
|
|
174
174
|
strokeLinecap: "round",
|
|
175
175
|
strokeLinejoin: "round",
|
|
176
176
|
strokeWidth: 2,
|
|
177
177
|
d: "M5 13l4 4L19 7"
|
|
178
178
|
})
|
|
179
179
|
})
|
|
180
|
-
}), /* @__PURE__ */
|
|
180
|
+
}), /* @__PURE__ */ h("div", {
|
|
181
181
|
className: "flex-1",
|
|
182
|
-
children: [/* @__PURE__ */
|
|
182
|
+
children: [/* @__PURE__ */ h("div", {
|
|
183
183
|
className: "flex items-center justify-between",
|
|
184
|
-
children: [/* @__PURE__ */
|
|
184
|
+
children: [/* @__PURE__ */ m("p", {
|
|
185
185
|
className: "font-medium text-text-primary",
|
|
186
186
|
children: e.name
|
|
187
|
-
}), /* @__PURE__ */
|
|
187
|
+
}), /* @__PURE__ */ m("span", {
|
|
188
188
|
className: "font-semibold text-text-primary",
|
|
189
189
|
children: i(e.totalValue, e.unit)
|
|
190
190
|
})]
|
|
191
|
-
}), /* @__PURE__ */
|
|
191
|
+
}), /* @__PURE__ */ h("div", {
|
|
192
192
|
className: "flex items-center gap-2 mt-1",
|
|
193
|
-
children: [/* @__PURE__ */
|
|
193
|
+
children: [/* @__PURE__ */ h("span", {
|
|
194
194
|
className: "text-sm text-text-secondary capitalize",
|
|
195
195
|
children: ["Type: ", e.limitType]
|
|
196
|
-
}), e.quotaType && /* @__PURE__ */
|
|
196
|
+
}), e.quotaType && /* @__PURE__ */ m("span", {
|
|
197
197
|
className: "px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-secondary rounded",
|
|
198
198
|
children: e.quotaType
|
|
199
199
|
})]
|
|
@@ -204,76 +204,76 @@ var x = () => {
|
|
|
204
204
|
})
|
|
205
205
|
})]
|
|
206
206
|
})]
|
|
207
|
-
}), /* @__PURE__ */
|
|
207
|
+
}), /* @__PURE__ */ h("div", {
|
|
208
208
|
className: "space-y-6 lg:sticky lg:top-6 lg:self-start",
|
|
209
|
-
children: [/* @__PURE__ */
|
|
209
|
+
children: [/* @__PURE__ */ h("div", {
|
|
210
210
|
className: "border border-border-seam rounded-card bg-bg-surface bg-accent-wash shadow-[var(--shadow-pop)] overflow-hidden",
|
|
211
|
-
children: [/* @__PURE__ */
|
|
211
|
+
children: [/* @__PURE__ */ m("div", {
|
|
212
212
|
className: "p-6 border-b border-border-seam bg-bg-sunken",
|
|
213
|
-
children: /* @__PURE__ */
|
|
213
|
+
children: /* @__PURE__ */ m("h2", {
|
|
214
214
|
className: "text-lg font-semibold text-text-primary",
|
|
215
215
|
children: s("billing.plans.subscribeTitle", "Subscribe")
|
|
216
216
|
})
|
|
217
|
-
}), /* @__PURE__ */
|
|
217
|
+
}), /* @__PURE__ */ h("div", {
|
|
218
218
|
className: "p-6 space-y-4",
|
|
219
219
|
children: [
|
|
220
|
-
/* @__PURE__ */
|
|
220
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("p", {
|
|
221
221
|
className: "text-sm text-text-secondary",
|
|
222
222
|
children: s("billing.plans.price", "Price")
|
|
223
|
-
}), /* @__PURE__ */
|
|
223
|
+
}), /* @__PURE__ */ h("p", {
|
|
224
224
|
className: "text-sm font-medium text-text-primary mt-0.5",
|
|
225
225
|
children: [
|
|
226
|
-
t(
|
|
226
|
+
t(y.price, y.currency),
|
|
227
227
|
" / ",
|
|
228
|
-
n(
|
|
228
|
+
n(y.duration)
|
|
229
229
|
]
|
|
230
230
|
})] }),
|
|
231
|
-
/* @__PURE__ */
|
|
231
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("p", {
|
|
232
232
|
className: "text-sm text-text-secondary",
|
|
233
233
|
children: s("billing.plans.billingPeriod", "Billing Period")
|
|
234
|
-
}), /* @__PURE__ */
|
|
234
|
+
}), /* @__PURE__ */ m("p", {
|
|
235
235
|
className: "text-sm text-text-primary mt-0.5",
|
|
236
|
-
children: r(
|
|
236
|
+
children: r(y.duration)
|
|
237
237
|
})] }),
|
|
238
|
-
/* @__PURE__ */
|
|
238
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("p", {
|
|
239
239
|
className: "text-sm text-text-secondary",
|
|
240
240
|
children: s("billing.plans.quotasIncluded", "Quotas Included")
|
|
241
|
-
}), /* @__PURE__ */
|
|
241
|
+
}), /* @__PURE__ */ m("p", {
|
|
242
242
|
className: "text-sm font-medium text-text-primary mt-0.5",
|
|
243
243
|
children: T.length
|
|
244
244
|
})] }),
|
|
245
|
-
/* @__PURE__ */
|
|
245
|
+
/* @__PURE__ */ m("button", {
|
|
246
246
|
type: "button",
|
|
247
|
-
onClick: () =>
|
|
247
|
+
onClick: () => p(`/checkout/${y.id}`),
|
|
248
248
|
className: "w-full px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors",
|
|
249
249
|
children: "Subscribe to Plan"
|
|
250
250
|
})
|
|
251
251
|
]
|
|
252
252
|
})]
|
|
253
|
-
}), /* @__PURE__ */
|
|
253
|
+
}), /* @__PURE__ */ m("button", {
|
|
254
254
|
type: "button",
|
|
255
|
-
onClick: () =>
|
|
255
|
+
onClick: () => p("/plans"),
|
|
256
256
|
className: "w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors",
|
|
257
257
|
children: "Back to Plans"
|
|
258
258
|
})]
|
|
259
259
|
})]
|
|
260
260
|
})]
|
|
261
|
-
}) : /* @__PURE__ */
|
|
261
|
+
}) : /* @__PURE__ */ m("div", {
|
|
262
262
|
className: "flex items-center justify-center h-full min-h-[400px]",
|
|
263
|
-
children: /* @__PURE__ */
|
|
263
|
+
children: /* @__PURE__ */ h("div", {
|
|
264
264
|
className: "text-center space-y-4",
|
|
265
265
|
children: [
|
|
266
|
-
/* @__PURE__ */
|
|
266
|
+
/* @__PURE__ */ m("h2", {
|
|
267
267
|
className: "text-lg font-semibold text-text-primary",
|
|
268
268
|
children: s("billing.plans.planNotFound", "Plan not found")
|
|
269
269
|
}),
|
|
270
|
-
/* @__PURE__ */
|
|
270
|
+
/* @__PURE__ */ m("p", {
|
|
271
271
|
className: "text-sm text-text-secondary",
|
|
272
272
|
children: C?.message || s("billing.plans.planNotFoundMessage", "The plan you are looking for does not exist.")
|
|
273
273
|
}),
|
|
274
|
-
/* @__PURE__ */
|
|
274
|
+
/* @__PURE__ */ m("button", {
|
|
275
275
|
type: "button",
|
|
276
|
-
onClick: () =>
|
|
276
|
+
onClick: () => p("/plans"),
|
|
277
277
|
className: "px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors",
|
|
278
278
|
children: "Back to Plans"
|
|
279
279
|
})
|
|
@@ -282,6 +282,6 @@ var x = () => {
|
|
|
282
282
|
});
|
|
283
283
|
};
|
|
284
284
|
//#endregion
|
|
285
|
-
export {
|
|
285
|
+
export { S as PlanBrowseDetailPage };
|
|
286
286
|
|
|
287
287
|
//# sourceMappingURL=PlanBrowseDetailPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlanBrowseDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlanBrowseDetailPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plan Browse Detail Page (User-facing)\n * View plan details and subscribe. No edit/toggle actions.\n */\n\nimport { type FC, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlan } from '../hooks';\nimport { ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n formatQuotaValue,\n getPlanFeatureQuantity,\n getPlanFeatureUnit,\n isServerError,\n} from '../../../shared/utils';\nimport type { PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ShowMore, PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport type { ReactNode } from 'react';\n\n/** Wraps children in a ShowMore progressive-disclosure region only when `when` is true. */\nconst ShowMoreIf: FC<{ when: boolean; collapsedHeight?: number; children: ReactNode }> = ({\n when,\n collapsedHeight,\n children,\n}) => (when ? <ShowMore collapsedHeight={collapsedHeight}>{children}</ShowMore> : <>{children}</>);\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n quotaType: string;\n limitType: string;\n totalValue: number;\n unit?: string;\n}\n\nfunction combineQuotas(features: PlanFeature[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limits = feature.quota.limits as { type?: string; value?: number } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = getPlanFeatureQuantity(feature);\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n quotaType: feature.quota.quotaType ?? '',\n limitType,\n totalValue: limitValue,\n unit: getPlanFeatureUnit(feature),\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nexport const PlanBrowseDetailPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { planId } = useParams<{ planId: string }>();\n const navigateTo = useBillingNavigate();\n const { plan, isLoading, error, refetch } = usePlan(planId);\n\n const combinedQuotas = useMemo(() => combineQuotas(plan?.features || []), [plan?.features]);\n\n if (isLoading) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-10 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n </div>\n );\n }\n\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.plans.serverUnavailable', 'Server Unavailable')}\n message={tr('billing.plans.unableToLoadPlanDetails', 'Unable to load plan details.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!plan) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.planNotFound', 'Plan not found')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {error?.message ||\n tr(\n 'billing.plans.planNotFoundMessage',\n 'The plan you are looking for does not exist.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"p-2 -ml-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors\"\n >\n <svg className=\"size-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {plan.name}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {formatPlanDurationLabel(plan.duration)} billing plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.plans.browseDetailPurpose',\n 'Everything this plan includes — its price, billing period and the full list of features and quotas — so you can confirm it fits before subscribing. Hit Subscribe when you are ready to check out.'\n )}\n </PagePurpose>\n </div>\n </div>\n\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n {/* Main Content */}\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Pricing Card — primary emphasis zone */}\n <EmphasisPanel className=\"overflow-hidden p-0\">\n <div className=\"p-6 border-b border-border-seam\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.pricing', 'Pricing')}\n </h2>\n </div>\n <div className=\"p-6\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </span>\n <span className=\"text-lg text-text-secondary\">\n / {formatPlanDuration(plan.duration)}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {tr('billing.plans.billed', 'Billed')}{' '}\n {plan.duration === 'monthly'\n ? tr('billing.plans.everyMonth', 'every month')\n : tr('billing.plans.annually', 'annually')}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Features Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.featuresAndQuotas', 'Features & Quotas')} (\n {combinedQuotas.length})\n </h2>\n </div>\n <div className=\"p-6\">\n {combinedQuotas.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.noFeatures', 'No features configured for this plan.')}\n </p>\n ) : (\n <ShowMoreIf when={combinedQuotas.length > 4} collapsedHeight={280}>\n <ul className=\"space-y-3\">\n {combinedQuotas.map((quota) => (\n <li\n key={quota.quotaId}\n className=\"flex items-start gap-3 p-3 rounded-lg bg-bg-sunken\"\n >\n <div className=\"size-8 rounded-full bg-status-success-bg-subtle flex items-center justify-center shrink-0\">\n <svg\n className=\"size-4 text-status-success-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between\">\n <p className=\"font-medium text-text-primary\">{quota.name}</p>\n <span className=\"font-semibold text-text-primary\">\n {formatQuotaValue(quota.totalValue, quota.unit)}\n </span>\n </div>\n <div className=\"flex items-center gap-2 mt-1\">\n <span className=\"text-sm text-text-secondary capitalize\">\n Type: {quota.limitType}\n </span>\n {quota.quotaType && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-secondary rounded\">\n {quota.quotaType}\n </span>\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n </ShowMoreIf>\n )}\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6 lg:sticky lg:top-6 lg:self-start\">\n {/* Subscribe Card — primary emphasis zone */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface bg-accent-wash shadow-[var(--shadow-pop)] overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.subscribeTitle', 'Subscribe')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">{tr('billing.plans.price', 'Price')}</p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {formatCurrency(plan.price, plan.currency)} / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.billingPeriod', 'Billing Period')}\n </p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatPlanDurationLabel(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.quotasIncluded', 'Quotas Included')}\n </p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {combinedQuotas.length}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"w-full px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\n </div>\n\n {/* Back */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;AAyBA,IAAM,KAAoF,EACxF,SACA,oBACA,kBACK,IAAO,kBAAC,GAAD;CAA2B;CAAkB;CAAoB,CAAA,GAAG,kBAAA,GAAA,EAAG,aAAY,CAAA;AAWjG,SAAS,EAAc,GAA0C;CAC/D,IAAM,oBAAW,IAAI,KAA4B;AACjD,MAAK,IAAM,KAAW,GAAU;AAC9B,MAAI,CAAC,EAAQ,MAAO;EACpB,IAAM,IAAU,EAAQ,MAAM,IAExB,IADS,EAAQ,MAAM,QACH,QAAQ,WAC5B,IAAa,EAAuB,EAAQ,EAC5C,IAAW,EAAS,IAAI,EAAQ;AACtC,EAAI,IACF,EAAS,cAAc,IAEvB,EAAS,IAAI,GAAS;GACpB;GACA,MAAM,EAAQ,MAAM,QAAQ;GAC5B,WAAW,EAAQ,MAAM,aAAa;GACtC;GACA,YAAY;GACZ,MAAM,EAAmB,EAAQ;GAClC,CAAC;;AAGN,QAAO,MAAM,KAAK,EAAS,QAAQ,CAAC;;AAGtC,IAAa,UAAiC;CAC5C,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,EAAI;AACzB,SAAO,MAAe,IAAM,IAAW;IAEnC,EAAE,cAAW,GAA+B,EAC5C,IAAa,GAAoB,EACjC,EAAE,SAAM,cAAW,UAAO,eAAY,EAAQ,EAAO,EAErD,IAAiB,QAAc,EAAc,GAAM,YAAY,EAAE,CAAC,EAAE,CAAC,GAAM,SAAS,CAAC;AAgE3F,QA9DI,IAEA,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAK,WAAU,8CAA+C,CAAA,EAC9D,kBAAC,OAAD,EAAK,WAAU,+CAAgD,CAAA,CAC3D;MACN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD,EAAK,WAAU,+CAAgD,CAAA,EAC/D,kBAAC,OAAD,EAAK,WAAU,iDAAkD,CAAA,CAC7D;;IACF,CAAA,EACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAK,WAAU,+CAAgD,CAAA,EAC/D,kBAAC,OAAD,EAAK,WAAU,kDAAmD,CAAA,CAC9D;MACF;KACF;MAIN,KAAS,EAAc,EAAM,GAE7B,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GACE,OAAO,EAAG,mCAAmC,qBAAqB;GAClE,SAAS,EAAG,yCAAyC,+BAA+B;GACpF,eAAe,GAAS;GACxB,WAAA;GACA,CAAA;EACE,CAAA,GAIL,IA2BH,kBAAC,OAAD;EAAK,WAAU;YAAf,CAEE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,SAAS;KACnC,WAAU;eAEV,kBAAC,OAAD;MAAK,WAAU;MAAS,MAAK;MAAO,SAAQ;MAAY,QAAO;gBAC7D,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;OACF,CAAA;MACE,CAAA;KACC,CAAA,EACT,kBAAC,OAAD,EAAA,UAAA;KACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAK;MACH,CAAA;KACL,kBAAC,KAAD;MAAG,WAAU;gBAAb,CACG,EAAwB,EAAK,SAAS,EAAC,gBACtC;;KACJ,kBAAC,GAAD;MAAa,WAAU;gBACpB,EACC,qCACA,qMACD;MACW,CAAA;KACV,EAAA,CAAA,CACF;OAEN,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,aAAa,EAAK,KAAK;IACjD,WAAU;cACX;IAEQ,CAAA,CACL;MAEN,kBAAC,OAAD;GAAK,WAAU;aAAf,CAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,GAAD;KAAe,WAAU;eAAzB,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,yBAAyB,UAAU;OACpC,CAAA;MACD,CAAA,EACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAe,EAAK,OAAO,EAAK,SAAS;QACrC,CAAA,EACP,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CAA8C,MACzC,EAAmB,EAAK,SAAS,CAC/B;UACH;UACN,kBAAC,KAAD;OAAG,WAAU;iBAAb;QACG,EAAG,wBAAwB,SAAS;QAAE;QACtC,EAAK,aAAa,YACf,EAAG,4BAA4B,cAAc,GAC7C,EAAG,0BAA0B,WAAW;QAC1C;SACA;QACQ;QAGhB,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACG,EAAG,mCAAmC,oBAAoB;QAAC;QAC3D,EAAe;QAAO;QACpB;;MACD,CAAA,EACN,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAe,WAAW,IACzB,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,4BAA4B,wCAAwC;OACtE,CAAA,GAEJ,kBAAC,GAAD;OAAY,MAAM,EAAe,SAAS;OAAG,iBAAiB;iBAC5D,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAe,KAAK,MACnB,kBAAC,MAAD;SAEE,WAAU;mBAFZ,CAIE,kBAAC,OAAD;UAAK,WAAU;oBACb,kBAAC,OAAD;WACE,WAAU;WACV,MAAK;WACL,SAAQ;WACR,QAAO;qBAEP,kBAAC,QAAD;YACE,eAAc;YACd,gBAAe;YACf,aAAa;YACb,GAAE;YACF,CAAA;WACE,CAAA;UACF,CAAA,EACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,KAAD;YAAG,WAAU;sBAAiC,EAAM;YAAS,CAAA,EAC7D,kBAAC,QAAD;YAAM,WAAU;sBACb,EAAiB,EAAM,YAAY,EAAM,KAAK;YAC1C,CAAA,CACH;cACN,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,QAAD;YAAM,WAAU;sBAAhB,CAAyD,UAChD,EAAM,UACR;eACN,EAAM,aACL,kBAAC,QAAD;YAAM,WAAU;sBACb,EAAM;YACF,CAAA,CAEL;aACF;YACH;WApCE,EAAM,QAoCR,CACL;QACC,CAAA;OACM,CAAA;MAEX,CAAA,CACF;OACF;OAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,gCAAgC,YAAY;OAC7C,CAAA;MACD,CAAA,EACN,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBAA+B,EAAG,uBAAuB,QAAQ;QAAK,CAAA,EACnF,kBAAC,KAAD;QAAG,WAAU;kBAAb;SACG,EAAe,EAAK,OAAO,EAAK,SAAS;SAAC;SAAI,EAAmB,EAAK,SAAS;SAC9E;UACA,EAAA,CAAA;OACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAG,+BAA+B,iBAAiB;QAClD,CAAA,EACJ,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAwB,EAAK,SAAS;QACrC,CAAA,CACA,EAAA,CAAA;OACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAG,gCAAgC,kBAAkB;QACpD,CAAA,EACJ,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAe;QACd,CAAA,CACA,EAAA,CAAA;OACN,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAW,aAAa,EAAK,KAAK;QACjD,WAAU;kBACX;QAEQ,CAAA;OACL;QACF;QAGN,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,SAAS;KACnC,WAAU;eACX;KAEQ,CAAA,CACL;MACF;KACF;MAnNJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,8BAA8B,iBAAiB;KAChD,CAAA;IACL,kBAAC,KAAD;KAAG,WAAU;eACV,GAAO,WACN,EACE,qCACA,+CACD;KACD,CAAA;IACJ,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,SAAS;KACnC,WAAU;eACX;KAEQ,CAAA;IACL;;EACF,CAAA"}
|
|
1
|
+
{"version":3,"file":"PlanBrowseDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlanBrowseDetailPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plan Browse Detail Page (User-facing)\n * View plan details and subscribe. No edit/toggle actions.\n */\n\nimport { type FC, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlan } from '../hooks';\nimport { ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n formatQuotaValue,\n getPlanFeatureQuantity,\n getPlanFeatureUnit,\n getQuotaDisplayName,\n isServerError,\n} from '../../../shared/utils';\nimport type { PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ShowMore, PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport type { ReactNode } from 'react';\n\n/** Wraps children in a ShowMore progressive-disclosure region only when `when` is true. */\nconst ShowMoreIf: FC<{ when: boolean; collapsedHeight?: number; children: ReactNode }> = ({\n when,\n collapsedHeight,\n children,\n}) => (when ? <ShowMore collapsedHeight={collapsedHeight}>{children}</ShowMore> : <>{children}</>);\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n quotaType: string;\n limitType: string;\n totalValue: number;\n unit?: string;\n}\n\nfunction combineQuotas(features: PlanFeature[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limits = feature.quota.limits as { type?: string; value?: number } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = getPlanFeatureQuantity(feature);\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: getQuotaDisplayName(feature),\n quotaType: feature.quota.quotaType ?? '',\n limitType,\n totalValue: limitValue,\n unit: getPlanFeatureUnit(feature),\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nexport const PlanBrowseDetailPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { planId } = useParams<{ planId: string }>();\n const navigateTo = useBillingNavigate();\n const { plan, isLoading, error, refetch } = usePlan(planId);\n\n const combinedQuotas = useMemo(() => combineQuotas(plan?.features || []), [plan?.features]);\n\n if (isLoading) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-10 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n </div>\n );\n }\n\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.plans.serverUnavailable', 'Server Unavailable')}\n message={tr('billing.plans.unableToLoadPlanDetails', 'Unable to load plan details.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!plan) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.planNotFound', 'Plan not found')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {error?.message ||\n tr(\n 'billing.plans.planNotFoundMessage',\n 'The plan you are looking for does not exist.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"p-2 -ml-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors\"\n >\n <svg className=\"size-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {plan.name}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {formatPlanDurationLabel(plan.duration)} billing plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.plans.browseDetailPurpose',\n 'Everything this plan includes — its price, billing period and the full list of features and quotas — so you can confirm it fits before subscribing. Hit Subscribe when you are ready to check out.'\n )}\n </PagePurpose>\n </div>\n </div>\n\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n {/* Main Content */}\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Pricing Card — primary emphasis zone */}\n <EmphasisPanel className=\"overflow-hidden p-0\">\n <div className=\"p-6 border-b border-border-seam\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.pricing', 'Pricing')}\n </h2>\n </div>\n <div className=\"p-6\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </span>\n <span className=\"text-lg text-text-secondary\">\n / {formatPlanDuration(plan.duration)}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {tr('billing.plans.billed', 'Billed')}{' '}\n {plan.duration === 'monthly'\n ? tr('billing.plans.everyMonth', 'every month')\n : tr('billing.plans.annually', 'annually')}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Features Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.featuresAndQuotas', 'Features & Quotas')} (\n {combinedQuotas.length})\n </h2>\n </div>\n <div className=\"p-6\">\n {combinedQuotas.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.noFeatures', 'No features configured for this plan.')}\n </p>\n ) : (\n <ShowMoreIf when={combinedQuotas.length > 4} collapsedHeight={280}>\n <ul className=\"space-y-3\">\n {combinedQuotas.map((quota) => (\n <li\n key={quota.quotaId}\n className=\"flex items-start gap-3 p-3 rounded-lg bg-bg-sunken\"\n >\n <div className=\"size-8 rounded-full bg-status-success-bg-subtle flex items-center justify-center shrink-0\">\n <svg\n className=\"size-4 text-status-success-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between\">\n <p className=\"font-medium text-text-primary\">{quota.name}</p>\n <span className=\"font-semibold text-text-primary\">\n {formatQuotaValue(quota.totalValue, quota.unit)}\n </span>\n </div>\n <div className=\"flex items-center gap-2 mt-1\">\n <span className=\"text-sm text-text-secondary capitalize\">\n Type: {quota.limitType}\n </span>\n {quota.quotaType && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-secondary rounded\">\n {quota.quotaType}\n </span>\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n </ShowMoreIf>\n )}\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6 lg:sticky lg:top-6 lg:self-start\">\n {/* Subscribe Card — primary emphasis zone */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface bg-accent-wash shadow-[var(--shadow-pop)] overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.subscribeTitle', 'Subscribe')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">{tr('billing.plans.price', 'Price')}</p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {formatCurrency(plan.price, plan.currency)} / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.billingPeriod', 'Billing Period')}\n </p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatPlanDurationLabel(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.quotasIncluded', 'Quotas Included')}\n </p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {combinedQuotas.length}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"w-full px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\n </div>\n\n {/* Back */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|