@nozle-js/react 0.5.1 → 0.5.3
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/README.md +88 -0
- package/dist/index.cjs +329 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -27
- package/dist/index.d.ts +22 -27
- package/dist/index.js +329 -85
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,6 +169,92 @@ function AfterPayment({ planCode }: { planCode: string }) {
|
|
|
169
169
|
|
|
170
170
|
Pre-built UI components with built-in styling. No CSS imports needed.
|
|
171
171
|
|
|
172
|
+
### `PricingTable`
|
|
173
|
+
|
|
174
|
+
Renders plan cards in a responsive grid with monthly/annual toggle, current plan detection, and CSS variable theming. Auto-fetches plans from the API if none are provided.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import { PricingTable } from "@nozle-js/react";
|
|
178
|
+
|
|
179
|
+
// Auto-fetch plans from API
|
|
180
|
+
<PricingTable
|
|
181
|
+
customerId="cust_123"
|
|
182
|
+
highlightPlan="pro"
|
|
183
|
+
onSelect={(plan) => handleUpgrade(plan)}
|
|
184
|
+
/>
|
|
185
|
+
|
|
186
|
+
// Or pass plans explicitly
|
|
187
|
+
<PricingTable
|
|
188
|
+
plans={[
|
|
189
|
+
{ code: "starter", name: "Starter", amount_cents: 2900, amount_currency: "USD", interval: "monthly" },
|
|
190
|
+
{ code: "pro", name: "Pro", amount_cents: 9900, amount_currency: "USD", interval: "monthly" },
|
|
191
|
+
]}
|
|
192
|
+
features={[
|
|
193
|
+
["10K API calls", "Email support"],
|
|
194
|
+
["100K API calls", "Priority support", "Analytics"],
|
|
195
|
+
]}
|
|
196
|
+
currentPlanCode="starter"
|
|
197
|
+
highlightPlan="pro"
|
|
198
|
+
onSelect={(plan) => console.log("Selected:", plan.code)}
|
|
199
|
+
showToggle={true}
|
|
200
|
+
enterpriseEmail="sales@example.com"
|
|
201
|
+
className="my-pricing"
|
|
202
|
+
/>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
| Prop | Type | Default | Description |
|
|
206
|
+
|------|------|---------|-------------|
|
|
207
|
+
| `customerId` | `string` | -- | Customer ID for auto-detecting current plan |
|
|
208
|
+
| `currentPlanCode` | `string` | -- | Explicitly set current plan (overrides auto-detect) |
|
|
209
|
+
| `plans` | `PricingPlan[]` | -- | Plans to display (auto-fetched from API if omitted) |
|
|
210
|
+
| `features` | `string[][]` | -- | Feature lists per plan (index-matched to plans array) |
|
|
211
|
+
| `onSelect` | `(plan: PricingPlan) => void` | -- | Called when a plan's CTA is clicked |
|
|
212
|
+
| `highlightPlan` | `string` | -- | Plan code to highlight as "Most Popular" |
|
|
213
|
+
| `enterpriseEmail` | `string` | -- | Email for enterprise plan "Contact Sales" button |
|
|
214
|
+
| `showToggle` | `boolean` | `true` | Show monthly/annual toggle (only when annual plans exist) |
|
|
215
|
+
| `className` | `string` | -- | CSS class for the outer wrapper |
|
|
216
|
+
|
|
217
|
+
#### PricingPlan type
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
interface PricingPlan {
|
|
221
|
+
code: string;
|
|
222
|
+
name: string;
|
|
223
|
+
amount_cents: number;
|
|
224
|
+
amount_currency: string;
|
|
225
|
+
interval: string;
|
|
226
|
+
description?: string;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### CSS Variable Theming
|
|
231
|
+
|
|
232
|
+
PricingTable uses CSS custom properties for full theming control. Set these on a parent element or `:root`:
|
|
233
|
+
|
|
234
|
+
```css
|
|
235
|
+
:root {
|
|
236
|
+
/* Component-specific overrides */
|
|
237
|
+
--nozle-pricing-bg: #ffffff;
|
|
238
|
+
--nozle-pricing-card-bg: #ffffff;
|
|
239
|
+
--nozle-pricing-highlight: #6366f1;
|
|
240
|
+
--nozle-pricing-border: #e5e7eb;
|
|
241
|
+
--nozle-pricing-radius: 12px;
|
|
242
|
+
|
|
243
|
+
/* Or use global Nozle variables (PricingTable falls back to these) */
|
|
244
|
+
--nozle-background: #ffffff;
|
|
245
|
+
--nozle-card: #ffffff;
|
|
246
|
+
--nozle-primary: #6366f1;
|
|
247
|
+
--nozle-border: #e5e7eb;
|
|
248
|
+
--nozle-radius: 12px;
|
|
249
|
+
--nozle-foreground: #111827;
|
|
250
|
+
--nozle-muted-foreground: #6b7280;
|
|
251
|
+
--nozle-muted: #f3f4f6;
|
|
252
|
+
--nozle-primary-foreground: #ffffff;
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The component-specific variables (`--nozle-pricing-*`) take precedence over the global ones (`--nozle-*`), which fall back to sensible defaults.
|
|
257
|
+
|
|
172
258
|
### `UsageMeter`
|
|
173
259
|
|
|
174
260
|
Displays usage with bar, ring, or minimal variants.
|
|
@@ -230,6 +316,8 @@ import type {
|
|
|
230
316
|
UsePlanResult,
|
|
231
317
|
UseCheckoutResult,
|
|
232
318
|
Plan,
|
|
319
|
+
PricingPlan,
|
|
320
|
+
PricingTableProps,
|
|
233
321
|
UsageMeterProps,
|
|
234
322
|
PlanBadgeProps,
|
|
235
323
|
UpgradePromptProps,
|
package/dist/index.cjs
CHANGED
|
@@ -303,6 +303,335 @@ function BillingPortalProvider({
|
|
|
303
303
|
}) {
|
|
304
304
|
return /* @__PURE__ */ jsxRuntime.jsx(BillingPortalContext.Provider, { value: { customerId, apiKey }, children });
|
|
305
305
|
}
|
|
306
|
+
var SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5" };
|
|
307
|
+
function fmtPrice(cents, cur) {
|
|
308
|
+
const s = SYMBOLS[cur] ?? cur + " ";
|
|
309
|
+
const val = cents % 100 === 0 ? String(cents / 100) : (cents / 100).toFixed(2);
|
|
310
|
+
return `${s}${val}`;
|
|
311
|
+
}
|
|
312
|
+
function isEnterprise(p) {
|
|
313
|
+
return /enterprise|custom/i.test(p.name + p.code);
|
|
314
|
+
}
|
|
315
|
+
function v(name, fb) {
|
|
316
|
+
return `var(${name}, ${fb})`;
|
|
317
|
+
}
|
|
318
|
+
var C = {
|
|
319
|
+
bg: v("--nozle-pricing-bg", v("--nozle-background", "#ffffff")),
|
|
320
|
+
cardBg: v("--nozle-pricing-card-bg", v("--nozle-card", "#ffffff")),
|
|
321
|
+
hl: v("--nozle-pricing-highlight", v("--nozle-primary", "#6366f1")),
|
|
322
|
+
border: v("--nozle-pricing-border", v("--nozle-border", "#e5e7eb")),
|
|
323
|
+
radius: v("--nozle-pricing-radius", v("--nozle-radius", "12px")),
|
|
324
|
+
text: v("--nozle-foreground", "#111827"),
|
|
325
|
+
muted: v("--nozle-muted-foreground", "#6b7280"),
|
|
326
|
+
mutedBg: v("--nozle-muted", "#f3f4f6"),
|
|
327
|
+
hlFg: v("--nozle-primary-foreground", "#ffffff")
|
|
328
|
+
};
|
|
329
|
+
function Check() {
|
|
330
|
+
return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) });
|
|
331
|
+
}
|
|
332
|
+
function Toggle({ isAnnual, onChange }) {
|
|
333
|
+
const pill = {
|
|
334
|
+
display: "inline-flex",
|
|
335
|
+
borderRadius: "9999px",
|
|
336
|
+
padding: "4px",
|
|
337
|
+
background: C.mutedBg,
|
|
338
|
+
gap: "2px"
|
|
339
|
+
};
|
|
340
|
+
const tab = (active) => ({
|
|
341
|
+
padding: "8px 20px",
|
|
342
|
+
borderRadius: "9999px",
|
|
343
|
+
border: "none",
|
|
344
|
+
background: active ? C.cardBg : "transparent",
|
|
345
|
+
color: active ? C.text : C.muted,
|
|
346
|
+
fontWeight: active ? 600 : 400,
|
|
347
|
+
fontSize: "0.875rem",
|
|
348
|
+
cursor: "pointer",
|
|
349
|
+
boxShadow: active ? "0 1px 3px rgba(0,0,0,0.08)" : "none",
|
|
350
|
+
transition: "all 0.15s ease"
|
|
351
|
+
});
|
|
352
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: pill, children: [
|
|
353
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => onChange(false), style: tab(!isAnnual), "aria-pressed": !isAnnual, children: "Monthly" }),
|
|
354
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: () => onChange(true), style: tab(isAnnual), "aria-pressed": isAnnual, children: [
|
|
355
|
+
"Annual",
|
|
356
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: {
|
|
357
|
+
marginLeft: "6px",
|
|
358
|
+
fontSize: "0.7rem",
|
|
359
|
+
fontWeight: 600,
|
|
360
|
+
color: isAnnual ? C.hl : C.muted
|
|
361
|
+
}, children: "Save 20%" })
|
|
362
|
+
] })
|
|
363
|
+
] }) });
|
|
364
|
+
}
|
|
365
|
+
function Card({
|
|
366
|
+
plan,
|
|
367
|
+
features,
|
|
368
|
+
isCurrent,
|
|
369
|
+
isHighlighted,
|
|
370
|
+
isEnt,
|
|
371
|
+
enterpriseEmail,
|
|
372
|
+
onSelect
|
|
373
|
+
}) {
|
|
374
|
+
const [hover, setHover] = React.useState(false);
|
|
375
|
+
const shadow = isHighlighted ? "0 8px 28px rgba(99,102,241,0.12), 0 2px 8px rgba(99,102,241,0.06)" : hover ? "0 8px 24px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.03)" : "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03)";
|
|
376
|
+
const interval = plan.interval === "yearly" ? "/year" : "/month";
|
|
377
|
+
const handleClick = () => {
|
|
378
|
+
if (isCurrent) return;
|
|
379
|
+
if (isEnt && enterpriseEmail) {
|
|
380
|
+
window.location.href = `mailto:${enterpriseEmail}?subject=Enterprise Plan Inquiry`;
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
onSelect?.();
|
|
384
|
+
};
|
|
385
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
386
|
+
"div",
|
|
387
|
+
{
|
|
388
|
+
onMouseEnter: () => setHover(true),
|
|
389
|
+
onMouseLeave: () => setHover(false),
|
|
390
|
+
style: {
|
|
391
|
+
position: "relative",
|
|
392
|
+
display: "flex",
|
|
393
|
+
flexDirection: "column",
|
|
394
|
+
background: C.cardBg,
|
|
395
|
+
border: `1px solid ${isHighlighted ? C.hl : C.border}`,
|
|
396
|
+
borderTopWidth: isHighlighted ? "3px" : "1px",
|
|
397
|
+
borderRadius: C.radius,
|
|
398
|
+
padding: "2rem",
|
|
399
|
+
transition: "box-shadow 0.2s ease, transform 0.2s ease",
|
|
400
|
+
boxShadow: shadow,
|
|
401
|
+
transform: hover && !isCurrent ? "translateY(-2px)" : "none"
|
|
402
|
+
},
|
|
403
|
+
children: [
|
|
404
|
+
isHighlighted && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
|
|
405
|
+
position: "absolute",
|
|
406
|
+
top: "-1px",
|
|
407
|
+
left: "50%",
|
|
408
|
+
transform: "translate(-50%, -50%)",
|
|
409
|
+
background: C.hl,
|
|
410
|
+
color: C.hlFg,
|
|
411
|
+
fontSize: "0.7rem",
|
|
412
|
+
fontWeight: 700,
|
|
413
|
+
padding: "4px 12px",
|
|
414
|
+
borderRadius: "9999px",
|
|
415
|
+
letterSpacing: "0.025em",
|
|
416
|
+
textTransform: "uppercase",
|
|
417
|
+
whiteSpace: "nowrap"
|
|
418
|
+
}, children: isCurrent ? "Current Plan" : "Most Popular" }),
|
|
419
|
+
isCurrent && !isHighlighted && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
|
|
420
|
+
position: "absolute",
|
|
421
|
+
top: "0",
|
|
422
|
+
left: "50%",
|
|
423
|
+
transform: "translate(-50%, -50%)",
|
|
424
|
+
background: C.mutedBg,
|
|
425
|
+
color: C.muted,
|
|
426
|
+
fontSize: "0.7rem",
|
|
427
|
+
fontWeight: 700,
|
|
428
|
+
padding: "4px 12px",
|
|
429
|
+
borderRadius: "9999px",
|
|
430
|
+
letterSpacing: "0.025em",
|
|
431
|
+
textTransform: "uppercase",
|
|
432
|
+
whiteSpace: "nowrap"
|
|
433
|
+
}, children: "Current Plan" }),
|
|
434
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1.25rem", marginTop: isHighlighted || isCurrent ? "0.5rem" : 0 }, children: [
|
|
435
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { style: {
|
|
436
|
+
fontSize: "1.25rem",
|
|
437
|
+
fontWeight: 700,
|
|
438
|
+
color: C.text,
|
|
439
|
+
margin: 0,
|
|
440
|
+
lineHeight: 1.3
|
|
441
|
+
}, children: plan.name }),
|
|
442
|
+
plan.description && /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
|
|
443
|
+
fontSize: "0.875rem",
|
|
444
|
+
color: C.muted,
|
|
445
|
+
margin: "6px 0 0",
|
|
446
|
+
lineHeight: 1.5
|
|
447
|
+
}, children: plan.description })
|
|
448
|
+
] }),
|
|
449
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginBottom: "1.5rem" }, children: isEnt ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "2.25rem", fontWeight: 700, color: C.text, lineHeight: 1 }, children: "Custom" }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
|
|
450
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "2.75rem", fontWeight: 700, color: C.text, lineHeight: 1, letterSpacing: "-0.025em" }, children: fmtPrice(plan.amount_cents, plan.amount_currency) }),
|
|
451
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.9375rem", color: C.muted, marginLeft: "4px" }, children: interval })
|
|
452
|
+
] }) }),
|
|
453
|
+
features && features.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { style: {
|
|
454
|
+
listStyle: "none",
|
|
455
|
+
padding: 0,
|
|
456
|
+
margin: "0 0 2rem",
|
|
457
|
+
display: "flex",
|
|
458
|
+
flexDirection: "column",
|
|
459
|
+
gap: "0.75rem",
|
|
460
|
+
flex: 1
|
|
461
|
+
}, children: features.map((feat) => /* @__PURE__ */ jsxRuntime.jsxs("li", { style: {
|
|
462
|
+
display: "flex",
|
|
463
|
+
alignItems: "flex-start",
|
|
464
|
+
gap: "10px",
|
|
465
|
+
fontSize: "0.875rem",
|
|
466
|
+
color: C.text,
|
|
467
|
+
lineHeight: 1.5
|
|
468
|
+
}, children: [
|
|
469
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: C.hl, marginTop: "2px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Check, {}) }),
|
|
470
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: feat })
|
|
471
|
+
] }, feat)) }),
|
|
472
|
+
(!features || features.length === 0) && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 } }),
|
|
473
|
+
onSelect && /* @__PURE__ */ jsxRuntime.jsx(
|
|
474
|
+
CTAButton,
|
|
475
|
+
{
|
|
476
|
+
onClick: handleClick,
|
|
477
|
+
disabled: isCurrent,
|
|
478
|
+
variant: isCurrent ? "current" : isEnt ? "outline" : isHighlighted ? "primary" : "secondary",
|
|
479
|
+
children: isCurrent ? "Current Plan" : isEnt ? "Contact Sales" : `Get ${plan.name}`
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
function CTAButton({
|
|
487
|
+
children,
|
|
488
|
+
onClick,
|
|
489
|
+
disabled,
|
|
490
|
+
variant
|
|
491
|
+
}) {
|
|
492
|
+
const [hover, setHover] = React.useState(false);
|
|
493
|
+
const base = {
|
|
494
|
+
width: "100%",
|
|
495
|
+
padding: "12px 24px",
|
|
496
|
+
borderRadius: "10px",
|
|
497
|
+
fontSize: "0.9375rem",
|
|
498
|
+
fontWeight: 600,
|
|
499
|
+
cursor: disabled ? "default" : "pointer",
|
|
500
|
+
border: "none",
|
|
501
|
+
transition: "all 0.15s ease",
|
|
502
|
+
lineHeight: 1.4
|
|
503
|
+
};
|
|
504
|
+
const variants = {
|
|
505
|
+
primary: {
|
|
506
|
+
background: hover && !disabled ? "#4f46e5" : C.hl,
|
|
507
|
+
color: C.hlFg
|
|
508
|
+
},
|
|
509
|
+
secondary: {
|
|
510
|
+
background: hover && !disabled ? "#f0f0f3" : C.mutedBg,
|
|
511
|
+
color: C.text,
|
|
512
|
+
border: `1px solid ${C.border}`
|
|
513
|
+
},
|
|
514
|
+
outline: {
|
|
515
|
+
background: hover && !disabled ? "rgba(99,102,241,0.04)" : "transparent",
|
|
516
|
+
color: C.hl,
|
|
517
|
+
border: `1.5px solid ${C.hl}`
|
|
518
|
+
},
|
|
519
|
+
current: {
|
|
520
|
+
background: C.mutedBg,
|
|
521
|
+
color: C.muted
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
525
|
+
"button",
|
|
526
|
+
{
|
|
527
|
+
onClick: disabled ? void 0 : onClick,
|
|
528
|
+
disabled,
|
|
529
|
+
onMouseEnter: () => setHover(true),
|
|
530
|
+
onMouseLeave: () => setHover(false),
|
|
531
|
+
style: { ...base, ...variants[variant] },
|
|
532
|
+
children
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
function Skeleton() {
|
|
537
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
|
|
538
|
+
display: "grid",
|
|
539
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
|
540
|
+
gap: "1.5rem"
|
|
541
|
+
}, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
|
|
542
|
+
background: C.cardBg,
|
|
543
|
+
border: `1px solid ${C.border}`,
|
|
544
|
+
borderRadius: C.radius,
|
|
545
|
+
padding: "2rem",
|
|
546
|
+
height: "380px"
|
|
547
|
+
}, children: [
|
|
548
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 20, width: "40%", borderRadius: 6, background: "#f0f0f0", marginBottom: 12 } }),
|
|
549
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 40, width: "50%", borderRadius: 6, background: "#f0f0f0", marginBottom: 24 } }),
|
|
550
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "80%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
|
|
551
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "70%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
|
|
552
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "60%", borderRadius: 6, background: "#f0f0f0" } })
|
|
553
|
+
] }, i)) });
|
|
554
|
+
}
|
|
555
|
+
function PricingTable({
|
|
556
|
+
customerId,
|
|
557
|
+
currentPlanCode,
|
|
558
|
+
plans: propPlans,
|
|
559
|
+
features,
|
|
560
|
+
onSelect,
|
|
561
|
+
showToggle = true,
|
|
562
|
+
highlightPlan,
|
|
563
|
+
className,
|
|
564
|
+
enterpriseEmail
|
|
565
|
+
}) {
|
|
566
|
+
const client = useNozleClient();
|
|
567
|
+
const [plans, setPlans] = React.useState(propPlans ?? []);
|
|
568
|
+
const [currentPlan, setCurrentPlan] = React.useState(currentPlanCode ?? null);
|
|
569
|
+
const [isAnnual, setIsAnnual] = React.useState(false);
|
|
570
|
+
const [loading, setLoading] = React.useState(!propPlans);
|
|
571
|
+
React.useEffect(() => {
|
|
572
|
+
if (propPlans) {
|
|
573
|
+
setPlans(propPlans);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
let cancelled = false;
|
|
577
|
+
client.fetch("/api/v1/plans").then(async (res) => {
|
|
578
|
+
if (!cancelled && res.ok) {
|
|
579
|
+
const data = await res.json();
|
|
580
|
+
setPlans(data.plans || []);
|
|
581
|
+
}
|
|
582
|
+
}).finally(() => {
|
|
583
|
+
if (!cancelled) setLoading(false);
|
|
584
|
+
});
|
|
585
|
+
return () => {
|
|
586
|
+
cancelled = true;
|
|
587
|
+
};
|
|
588
|
+
}, [client, propPlans]);
|
|
589
|
+
React.useEffect(() => {
|
|
590
|
+
if (currentPlanCode !== void 0) {
|
|
591
|
+
setCurrentPlan(currentPlanCode);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (!customerId) return;
|
|
595
|
+
let cancelled = false;
|
|
596
|
+
client.fetch(`/api/v1/billing/status?customer_id=${encodeURIComponent(customerId)}`).then(async (res) => {
|
|
597
|
+
if (!cancelled && res.ok) {
|
|
598
|
+
const data = await res.json();
|
|
599
|
+
setCurrentPlan(data.plan ?? data.planName ?? null);
|
|
600
|
+
}
|
|
601
|
+
}).catch(() => {
|
|
602
|
+
});
|
|
603
|
+
return () => {
|
|
604
|
+
cancelled = true;
|
|
605
|
+
};
|
|
606
|
+
}, [client, customerId, currentPlanCode]);
|
|
607
|
+
const hasAnnual = plans.some((p) => p.interval === "yearly");
|
|
608
|
+
const filtered = showToggle && hasAnnual ? plans.filter((p) => p.interval === (isAnnual ? "yearly" : "monthly") || isEnterprise(p)) : plans;
|
|
609
|
+
if (loading) return /* @__PURE__ */ jsxRuntime.jsx(Skeleton, {});
|
|
610
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: {
|
|
611
|
+
fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
612
|
+
color: C.text,
|
|
613
|
+
background: C.bg
|
|
614
|
+
}, children: [
|
|
615
|
+
showToggle && hasAnnual && /* @__PURE__ */ jsxRuntime.jsx(Toggle, { isAnnual, onChange: setIsAnnual }),
|
|
616
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: {
|
|
617
|
+
display: "grid",
|
|
618
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
|
619
|
+
gap: "1.5rem"
|
|
620
|
+
}, children: filtered.map((plan, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
621
|
+
Card,
|
|
622
|
+
{
|
|
623
|
+
plan,
|
|
624
|
+
features: features?.[i],
|
|
625
|
+
isCurrent: currentPlan === plan.code,
|
|
626
|
+
isHighlighted: highlightPlan === plan.code,
|
|
627
|
+
isEnt: isEnterprise(plan),
|
|
628
|
+
enterpriseEmail,
|
|
629
|
+
onSelect: onSelect ? () => onSelect(plan) : void 0
|
|
630
|
+
},
|
|
631
|
+
plan.code
|
|
632
|
+
)) })
|
|
633
|
+
] });
|
|
634
|
+
}
|
|
306
635
|
function PlanCard({
|
|
307
636
|
id,
|
|
308
637
|
name,
|
|
@@ -447,91 +776,6 @@ function PlanCard({
|
|
|
447
776
|
}
|
|
448
777
|
);
|
|
449
778
|
}
|
|
450
|
-
function PricingTable({
|
|
451
|
-
plans,
|
|
452
|
-
currentPlanId,
|
|
453
|
-
onSelect
|
|
454
|
-
}) {
|
|
455
|
-
const [isAnnual, setIsAnnual] = React.useState(false);
|
|
456
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
457
|
-
"div",
|
|
458
|
-
{
|
|
459
|
-
style: {
|
|
460
|
-
background: "var(--nozle-background, var(--background))",
|
|
461
|
-
color: "var(--nozle-foreground, var(--foreground))"
|
|
462
|
-
},
|
|
463
|
-
children: [
|
|
464
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
465
|
-
"div",
|
|
466
|
-
{
|
|
467
|
-
style: {
|
|
468
|
-
display: "flex",
|
|
469
|
-
gap: "0.5rem",
|
|
470
|
-
justifyContent: "center",
|
|
471
|
-
marginBottom: "1.5rem"
|
|
472
|
-
},
|
|
473
|
-
children: [
|
|
474
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
475
|
-
"button",
|
|
476
|
-
{
|
|
477
|
-
onClick: () => setIsAnnual(false),
|
|
478
|
-
style: {
|
|
479
|
-
padding: "0.5rem 1rem",
|
|
480
|
-
borderRadius: "var(--nozle-radius, 0.5rem)",
|
|
481
|
-
border: "1px solid var(--nozle-border, var(--border))",
|
|
482
|
-
background: !isAnnual ? "var(--nozle-primary, var(--primary))" : "transparent",
|
|
483
|
-
color: !isAnnual ? "var(--nozle-primary-foreground, var(--primary-foreground))" : "var(--nozle-foreground, var(--foreground))",
|
|
484
|
-
cursor: "pointer",
|
|
485
|
-
fontWeight: !isAnnual ? 600 : 400
|
|
486
|
-
},
|
|
487
|
-
"aria-pressed": !isAnnual,
|
|
488
|
-
children: "Monthly"
|
|
489
|
-
}
|
|
490
|
-
),
|
|
491
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
492
|
-
"button",
|
|
493
|
-
{
|
|
494
|
-
onClick: () => setIsAnnual(true),
|
|
495
|
-
style: {
|
|
496
|
-
padding: "0.5rem 1rem",
|
|
497
|
-
borderRadius: "var(--nozle-radius, 0.5rem)",
|
|
498
|
-
border: "1px solid var(--nozle-border, var(--border))",
|
|
499
|
-
background: isAnnual ? "var(--nozle-primary, var(--primary))" : "transparent",
|
|
500
|
-
color: isAnnual ? "var(--nozle-primary-foreground, var(--primary-foreground))" : "var(--nozle-foreground, var(--foreground))",
|
|
501
|
-
cursor: "pointer",
|
|
502
|
-
fontWeight: isAnnual ? 600 : 400
|
|
503
|
-
},
|
|
504
|
-
"aria-pressed": isAnnual,
|
|
505
|
-
children: "Annual"
|
|
506
|
-
}
|
|
507
|
-
)
|
|
508
|
-
]
|
|
509
|
-
}
|
|
510
|
-
),
|
|
511
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
512
|
-
"div",
|
|
513
|
-
{
|
|
514
|
-
style: {
|
|
515
|
-
display: "grid",
|
|
516
|
-
gridTemplateColumns: `repeat(${Math.min(plans.length, 3)}, 1fr)`,
|
|
517
|
-
gap: "1rem"
|
|
518
|
-
},
|
|
519
|
-
children: plans.map((plan) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
520
|
-
PlanCard,
|
|
521
|
-
{
|
|
522
|
-
...plan,
|
|
523
|
-
isAnnual,
|
|
524
|
-
isCurrent: plan.id === currentPlanId,
|
|
525
|
-
onSelect: onSelect ? () => onSelect(plan.id) : void 0
|
|
526
|
-
},
|
|
527
|
-
plan.id
|
|
528
|
-
))
|
|
529
|
-
}
|
|
530
|
-
)
|
|
531
|
-
]
|
|
532
|
-
}
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
779
|
function PlanComparison({
|
|
536
780
|
plans,
|
|
537
781
|
features
|