@lukoweb/apitogo-plugin-dev-portal-billing 0.2.30 → 0.2.34

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.
@@ -0,0 +1,233 @@
1
+ import { h as pickFeaturedPlanSku, n as useDevPortalPreviewMode, p as parseManifestPlans } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
2
+ import { t as PricingCard } from "./PricingCard-Wra2FUX9.mjs";
3
+ import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
+ import { t as usePlans } from "./usePlans-DUex2tzT.mjs";
5
+ import { t as isPreviewPricingEnabled } from "./pricing-config-CfhRX9tk.mjs";
6
+ import { i as isPricingLoading, n as buildCompareRows } from "./billing-state-pnT0H1Lm.mjs";
7
+ import { Head, Heading } from "@lukoweb/apitogo/components";
8
+ import { Navigate } from "@lukoweb/apitogo/router";
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ import { useAuth } from "@lukoweb/apitogo/hooks";
11
+
12
+ //#region src/pages/PricingPage.tsx
13
+ const DEFAULT_COMPARE_ROWS = [
14
+ {
15
+ label: "Monthly requests",
16
+ values: [
17
+ "1,000",
18
+ "50,000",
19
+ "500,000",
20
+ "Unlimited"
21
+ ]
22
+ },
23
+ {
24
+ label: "Rate limit",
25
+ values: [
26
+ "10/sec",
27
+ "50/sec",
28
+ "200/sec",
29
+ "Custom"
30
+ ]
31
+ },
32
+ {
33
+ label: "Team seats",
34
+ values: [
35
+ "1",
36
+ "1",
37
+ "5",
38
+ "Unlimited"
39
+ ]
40
+ },
41
+ {
42
+ label: "Webhooks",
43
+ values: [
44
+ "—",
45
+ "Included",
46
+ "Included",
47
+ "Included"
48
+ ]
49
+ },
50
+ {
51
+ label: "SSO",
52
+ values: [
53
+ "—",
54
+ "—",
55
+ "—",
56
+ "Included"
57
+ ]
58
+ },
59
+ {
60
+ label: "Support",
61
+ values: [
62
+ "—",
63
+ "Email",
64
+ "Priority",
65
+ "Dedicated SLA"
66
+ ]
67
+ }
68
+ ];
69
+ const DEFAULT_FAQS = [{
70
+ question: "What happens if I exceed my quota?",
71
+ answer: "Requests over your monthly quota return a 429 response so you can test rate-limit handling instead of being cut off unexpectedly."
72
+ }, {
73
+ question: "Can I change plans later?",
74
+ answer: "Yes, upgrade or downgrade any time from your billing page. Changes apply on your next billing cycle."
75
+ }];
76
+ const PricingPage = () => {
77
+ const { pricing, previewPlans } = useDevPortalBillingConfig();
78
+ const isPreview = useDevPortalPreviewMode();
79
+ const pricingEnabled = isPreviewPricingEnabled() || isPreview;
80
+ const livePlans = usePlans();
81
+ const catalog = isPreview ? parseManifestPlans(previewPlans) : livePlans.data;
82
+ const { isPending: authPending } = useAuth();
83
+ const isLoading = isPricingLoading({
84
+ isPreview,
85
+ plansLoading: livePlans.isLoading,
86
+ hasPlans: Boolean(catalog?.plans?.length),
87
+ plansFailed: livePlans.isError,
88
+ authPending
89
+ });
90
+ const isError = isPreview ? false : livePlans.isError;
91
+ if (!pricingEnabled) return /* @__PURE__ */ jsx(Navigate, {
92
+ to: "/",
93
+ replace: true
94
+ });
95
+ const title = pricing?.title ?? "Pay for what you call";
96
+ const subtitle = pricing?.subtitle ?? "Every plan includes the full endpoint catalog. Higher tiers raise your request quota, rate limit, and support level.";
97
+ const plans = catalog?.plans ?? [];
98
+ const featuredSku = pickFeaturedPlanSku(plans);
99
+ const compareRows = buildCompareRows({
100
+ plans,
101
+ configuredRows: pricing?.compareRows,
102
+ isPreview,
103
+ sampleRows: DEFAULT_COMPARE_ROWS
104
+ });
105
+ const faqs = pricing?.faqs && pricing.faqs.length > 0 ? pricing.faqs : DEFAULT_FAQS;
106
+ const compareHeaders = plans.length > 0 ? plans.map((p) => p.displayName) : [
107
+ "Free",
108
+ "Developer",
109
+ "Team",
110
+ "Enterprise"
111
+ ];
112
+ return /* @__PURE__ */ jsxs("div", {
113
+ className: "w-full",
114
+ children: [
115
+ /* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: title }), /* @__PURE__ */ jsx("meta", {
116
+ name: "description",
117
+ content: subtitle
118
+ })] }),
119
+ /* @__PURE__ */ jsxs("section", {
120
+ className: "px-6 pb-4 pt-[clamp(3rem,6vw,4.5rem)] text-center",
121
+ children: [
122
+ /* @__PURE__ */ jsx("p", {
123
+ className: "text-xs font-bold uppercase tracking-[0.08em] text-primary",
124
+ children: "Pricing"
125
+ }),
126
+ /* @__PURE__ */ jsx(Heading, {
127
+ level: 1,
128
+ className: "mt-3 text-[clamp(2rem,3.5vw,3rem)] font-extrabold tracking-[-0.02em]",
129
+ "data-testid": "title",
130
+ children: title
131
+ }),
132
+ /* @__PURE__ */ jsx("p", {
133
+ className: "mx-auto mt-3.5 max-w-[34rem] text-lg leading-relaxed text-muted-foreground",
134
+ "data-testid": "subtitle",
135
+ children: subtitle
136
+ })
137
+ ]
138
+ }),
139
+ /* @__PURE__ */ jsxs("section", {
140
+ className: "px-6 pb-16 pt-8",
141
+ children: [
142
+ isLoading ? /* @__PURE__ */ jsx("p", {
143
+ className: "text-center text-muted-foreground",
144
+ children: "Loading plans…"
145
+ }) : null,
146
+ isError ? /* @__PURE__ */ jsx("p", {
147
+ className: "text-center text-destructive",
148
+ children: "Unable to load plans."
149
+ }) : null,
150
+ plans.length > 0 ? /* @__PURE__ */ jsx("div", {
151
+ className: "mx-auto grid max-w-[1200px] grid-cols-1 items-stretch gap-5 sm:grid-cols-2 lg:grid-cols-4",
152
+ children: plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
153
+ plan,
154
+ mode: "public",
155
+ featured: plan.sku === featuredSku
156
+ }, plan.sku))
157
+ }) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
158
+ className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
159
+ "data-testid": "pricing-empty",
160
+ children: isPreview ? /* @__PURE__ */ jsxs(Fragment, { children: [
161
+ "No plans are configured yet. Add plans in",
162
+ " ",
163
+ /* @__PURE__ */ jsx("code", {
164
+ className: "rounded bg-muted px-1.5 py-0.5",
165
+ children: "apitogo.config.json"
166
+ }),
167
+ " ",
168
+ "and set",
169
+ " ",
170
+ /* @__PURE__ */ jsx("code", {
171
+ className: "rounded bg-muted px-1.5 py-0.5",
172
+ children: "modules.userPanel.plans.enabled"
173
+ }),
174
+ " ",
175
+ "to true."
176
+ ] }) : "Pricing is not available yet. Please check back soon."
177
+ }) : null
178
+ ]
179
+ }),
180
+ isLoading || compareRows.length === 0 ? null : /* @__PURE__ */ jsx("section", {
181
+ className: "border-t border-border bg-muted/40 px-6 py-16",
182
+ "data-testid": "pricing-compare",
183
+ children: /* @__PURE__ */ jsxs("div", {
184
+ className: "mx-auto max-w-[1200px]",
185
+ children: [/* @__PURE__ */ jsx("h2", {
186
+ className: "mb-6 text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
187
+ children: "Compare plans"
188
+ }), /* @__PURE__ */ jsx("div", {
189
+ className: "overflow-x-auto rounded-xl border border-border bg-card",
190
+ children: /* @__PURE__ */ jsxs("div", {
191
+ className: "min-w-[720px]",
192
+ children: [/* @__PURE__ */ jsxs("div", {
193
+ className: "grid gap-0 border-b border-border px-5 py-3.5 text-[0.78125rem] font-bold uppercase tracking-[0.04em] text-muted-foreground",
194
+ style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
195
+ children: [/* @__PURE__ */ jsx("div", { children: "Feature" }), compareHeaders.map((header) => /* @__PURE__ */ jsx("div", { children: header }, header))]
196
+ }), compareRows.map((row) => /* @__PURE__ */ jsxs("div", {
197
+ className: "grid gap-0 border-b border-border px-5 py-3.5 text-[0.84375rem] text-muted-foreground last:border-b-0",
198
+ style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
199
+ children: [/* @__PURE__ */ jsx("div", {
200
+ className: "font-medium text-foreground",
201
+ children: row.label
202
+ }), compareHeaders.map((header, index) => /* @__PURE__ */ jsx("div", { children: row.values[index] ?? "—" }, `${row.label}-${header}`))]
203
+ }, row.label))]
204
+ })
205
+ })]
206
+ })
207
+ }),
208
+ /* @__PURE__ */ jsx("section", {
209
+ className: "px-6 py-16",
210
+ "data-testid": "pricing-faq",
211
+ children: /* @__PURE__ */ jsxs("div", {
212
+ className: "mx-auto max-w-4xl",
213
+ children: [/* @__PURE__ */ jsx("h2", {
214
+ className: "mb-8 text-center text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
215
+ children: "Frequently asked questions"
216
+ }), /* @__PURE__ */ jsx("div", { children: faqs.map((faq) => /* @__PURE__ */ jsxs("div", {
217
+ className: "border-b border-border py-5 last:border-b-0",
218
+ children: [/* @__PURE__ */ jsx("div", {
219
+ className: "text-[0.9375rem] font-bold",
220
+ children: faq.question
221
+ }), /* @__PURE__ */ jsx("p", {
222
+ className: "mt-2 text-sm leading-relaxed text-muted-foreground",
223
+ children: faq.answer
224
+ })]
225
+ }, faq.question)) })]
226
+ })
227
+ })
228
+ ]
229
+ });
230
+ };
231
+
232
+ //#endregion
233
+ export { PricingPage as default };
@@ -0,0 +1,226 @@
1
+ import { i as CREDENTIALS_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
2
+ import { c as isActiveSubscription, i as formatBillingPeriod, m as parseMonthlyQuotaCalls, o as formatPlanPrice, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
3
+ import "./DevPortalBillingContext-CR83s4LO.mjs";
4
+ import { t as useMySubscriptions } from "./useMySubscriptions-CLh3eFY4.mjs";
5
+ import { t as usePlans } from "./usePlans-DUex2tzT.mjs";
6
+ import { t as useUsage } from "./useUsage-Cx2GB3wg.mjs";
7
+ import { r as checkoutConfirmingText } from "./billing-state-pnT0H1Lm.mjs";
8
+ import { Head, Spinner } from "@lukoweb/apitogo/components";
9
+ import { Link as Link$1, useSearchParams } from "@lukoweb/apitogo/router";
10
+ import { useEffect } from "react";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ import { useMutation } from "@lukoweb/apitogo/react-query";
13
+ import { useAuth } from "@lukoweb/apitogo/hooks";
14
+ import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
15
+
16
+ //#region src/hooks/useBillingPortal.ts
17
+ function useBillingPortal() {
18
+ const apiBaseUrl = useDevPortalApiBaseUrl();
19
+ return useMutation({ mutationFn: async (returnUrl) => {
20
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
21
+ const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/billing-portal`, {
22
+ method: "POST",
23
+ headers: {
24
+ Accept: "application/json",
25
+ "Content-Type": "application/json"
26
+ },
27
+ credentials: "include",
28
+ body: JSON.stringify({ returnUrl: returnUrl ?? `${window.location.origin}/account/subscriptions` })
29
+ });
30
+ if (!response.ok) throw new Error("Unable to open billing portal.");
31
+ const body = await response.json();
32
+ if (!body.url) throw new Error("Billing portal URL was not returned.");
33
+ window.location.assign(body.url);
34
+ } });
35
+ }
36
+
37
+ //#endregion
38
+ //#region src/pages/SubscriptionsPage.tsx
39
+ const SubscriptionsPage = () => {
40
+ const auth = useAuth();
41
+ const [searchParams] = useSearchParams();
42
+ const checkoutSuccess = searchParams.get("checkout") === "success";
43
+ const checkoutPaid = searchParams.get("paid") !== "0";
44
+ const { data: me, isLoading, refetch } = useMySubscriptions();
45
+ const { data: catalog } = usePlans();
46
+ const billingPortal = useBillingPortal();
47
+ const { data: usage } = useUsage("mtd");
48
+ const planBySku = new Map((catalog?.plans ?? []).map((plan) => [plan.sku, plan]));
49
+ const subscriptions = me?.subscriptions ?? [];
50
+ const activeSubscription = subscriptions.find((sub) => isActiveSubscription(sub.status));
51
+ const activePlan = activeSubscription ? planBySku.get(activeSubscription.planSku) : void 0;
52
+ const confirmingPayment = checkoutSuccess && !isLoading && !activeSubscription;
53
+ useEffect(() => {
54
+ if (!confirmingPayment) return;
55
+ let attempts = 0;
56
+ const timer = setInterval(() => {
57
+ attempts += 1;
58
+ refetch();
59
+ if (attempts >= 30) clearInterval(timer);
60
+ }, 2e3);
61
+ return () => clearInterval(timer);
62
+ }, [confirmingPayment, refetch]);
63
+ const monthlyQuota = activePlan ? parseMonthlyQuotaCalls(activePlan.limitsJson) : null;
64
+ const usedRequests = usage?.requestCount ?? 0;
65
+ const quotaPct = monthlyQuota && monthlyQuota > 0 ? Math.min(100, Math.round(usedRequests / monthlyQuota * 100)) : null;
66
+ return /* @__PURE__ */ jsxs("div", {
67
+ className: "px-8 py-7",
68
+ children: [
69
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Billing" }) }),
70
+ /* @__PURE__ */ jsx("h1", {
71
+ className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
72
+ children: "Billing"
73
+ }),
74
+ /* @__PURE__ */ jsx("p", {
75
+ className: "mb-8 text-muted-foreground",
76
+ children: "Manage your plan, payment method, and invoices."
77
+ }),
78
+ confirmingPayment ? /* @__PURE__ */ jsx("div", {
79
+ className: "mb-6 rounded-xl border border-border bg-card p-4 text-sm",
80
+ children: checkoutConfirmingText(checkoutPaid)
81
+ }) : null,
82
+ checkoutSuccess && activeSubscription ? /* @__PURE__ */ jsxs("div", {
83
+ className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
84
+ children: [
85
+ "Your subscription is active.",
86
+ " ",
87
+ /* @__PURE__ */ jsx(Link$1, {
88
+ to: CREDENTIALS_PATH,
89
+ className: "font-medium underline",
90
+ children: "View your API credentials"
91
+ }),
92
+ "."
93
+ ]
94
+ }) : null,
95
+ isLoading ? /* @__PURE__ */ jsx("p", {
96
+ className: "text-muted-foreground",
97
+ children: "Loading billing details…"
98
+ }) : null,
99
+ !isLoading && activePlan && activeSubscription ? /* @__PURE__ */ jsxs("div", {
100
+ className: "mb-8 rounded-xl border border-border bg-card p-6",
101
+ children: [
102
+ /* @__PURE__ */ jsx("p", {
103
+ className: "text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
104
+ children: "Current plan"
105
+ }),
106
+ /* @__PURE__ */ jsx("h2", {
107
+ className: "mt-2 text-2xl font-bold",
108
+ children: activePlan.displayName
109
+ }),
110
+ /* @__PURE__ */ jsxs("p", {
111
+ className: "mt-1 text-sm text-muted-foreground",
112
+ children: [
113
+ formatPlanPrice(activePlan),
114
+ " ·",
115
+ " ",
116
+ formatBillingPeriod(activePlan.billingPeriod)
117
+ ]
118
+ }),
119
+ activeSubscription.currentPeriodEnd ? /* @__PURE__ */ jsxs("p", {
120
+ className: "mt-3 text-sm text-muted-foreground",
121
+ children: [
122
+ "Renews",
123
+ " ",
124
+ new Date(activeSubscription.currentPeriodEnd).toLocaleDateString()
125
+ ]
126
+ }) : null,
127
+ quotaPct != null && monthlyQuota != null ? /* @__PURE__ */ jsxs("div", {
128
+ className: "mt-5",
129
+ children: [/* @__PURE__ */ jsxs("div", {
130
+ className: "mb-1.5 flex justify-between text-xs text-muted-foreground",
131
+ children: [/* @__PURE__ */ jsx("span", { children: "Monthly requests" }), /* @__PURE__ */ jsxs("span", { children: [
132
+ usedRequests.toLocaleString(),
133
+ " /",
134
+ " ",
135
+ monthlyQuota.toLocaleString()
136
+ ] })]
137
+ }), /* @__PURE__ */ jsx("div", {
138
+ className: "h-2 rounded-full bg-muted",
139
+ children: /* @__PURE__ */ jsx("div", {
140
+ className: "h-full rounded-full bg-primary transition-[width]",
141
+ style: { width: `${quotaPct}%` }
142
+ })
143
+ })]
144
+ }) : null
145
+ ]
146
+ }) : null,
147
+ !isLoading && subscriptions.length === 0 ? /* @__PURE__ */ jsxs("div", {
148
+ className: "mb-8 rounded-xl border border-dashed p-8 text-center",
149
+ children: [/* @__PURE__ */ jsx("p", {
150
+ className: "text-muted-foreground",
151
+ children: "You do not have an active subscription yet."
152
+ }), /* @__PURE__ */ jsx(Button$1, {
153
+ className: "mt-4",
154
+ asChild: true,
155
+ children: /* @__PURE__ */ jsx(Link$1, {
156
+ to: ACCOUNT_PLANS_PATH,
157
+ children: "View plans"
158
+ })
159
+ })]
160
+ }) : null,
161
+ !isLoading && subscriptions.length > 1 ? /* @__PURE__ */ jsx("div", {
162
+ className: "mb-8 space-y-4",
163
+ children: subscriptions.map((subscription) => {
164
+ const plan = planBySku.get(subscription.planSku);
165
+ return /* @__PURE__ */ jsx("article", {
166
+ className: "rounded-xl border bg-card p-5",
167
+ children: /* @__PURE__ */ jsxs("div", {
168
+ className: "flex flex-wrap items-start justify-between gap-4",
169
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
170
+ className: "font-semibold",
171
+ children: plan?.displayName ?? subscription.planSku
172
+ }), plan ? /* @__PURE__ */ jsxs("p", {
173
+ className: "mt-1 text-sm text-muted-foreground",
174
+ children: [
175
+ formatPlanPrice(plan),
176
+ " ·",
177
+ " ",
178
+ formatBillingPeriod(plan.billingPeriod)
179
+ ]
180
+ }) : null] }), /* @__PURE__ */ jsx("span", {
181
+ className: "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground",
182
+ children: subscription.status
183
+ })]
184
+ })
185
+ }, `${subscription.planSku}-${subscription.status}`);
186
+ })
187
+ }) : null,
188
+ /* @__PURE__ */ jsxs("div", {
189
+ className: "flex flex-wrap gap-3",
190
+ children: [
191
+ /* @__PURE__ */ jsx(Button$1, {
192
+ variant: "outline",
193
+ asChild: true,
194
+ children: /* @__PURE__ */ jsx(Link$1, {
195
+ to: ACCOUNT_PLANS_PATH,
196
+ children: "Change plan"
197
+ })
198
+ }),
199
+ auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
200
+ variant: "default",
201
+ disabled: billingPortal.isPending,
202
+ onClick: () => {
203
+ billingPortal.mutate();
204
+ },
205
+ children: billingPortal.isPending ? /* @__PURE__ */ jsx(Spinner, {}) : "Manage payment & invoices"
206
+ }) : null,
207
+ auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
208
+ variant: "secondary",
209
+ asChild: true,
210
+ children: /* @__PURE__ */ jsx(Link$1, {
211
+ to: CREDENTIALS_PATH,
212
+ children: "API credentials"
213
+ })
214
+ }) : null
215
+ ]
216
+ }),
217
+ billingPortal.isError ? /* @__PURE__ */ jsx("p", {
218
+ className: "mt-4 text-sm text-destructive",
219
+ children: billingPortal.error instanceof Error ? billingPortal.error.message : "Unable to open billing portal."
220
+ }) : null
221
+ ]
222
+ });
223
+ };
224
+
225
+ //#endregion
226
+ export { SubscriptionsPage as default };
@@ -0,0 +1,203 @@
1
+ import "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
2
+ import { t as useUsage } from "./useUsage-Cx2GB3wg.mjs";
3
+ import { Head } from "@lukoweb/apitogo/components";
4
+ import { Link as Link$1 } from "@lukoweb/apitogo/router";
5
+ import { useState } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { useAuth } from "@lukoweb/apitogo/hooks";
8
+ import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
9
+
10
+ //#region src/pages/UsagePage.tsx
11
+ const RANGES = [
12
+ {
13
+ value: "24h",
14
+ label: "24h"
15
+ },
16
+ {
17
+ value: "7d",
18
+ label: "7d"
19
+ },
20
+ {
21
+ value: "30d",
22
+ label: "30d"
23
+ },
24
+ {
25
+ value: "90d",
26
+ label: "90d"
27
+ }
28
+ ];
29
+ function formatNumber(value) {
30
+ return new Intl.NumberFormat().format(value);
31
+ }
32
+ function formatPct(rate) {
33
+ return `${(rate * 100).toFixed(rate >= .1 ? 1 : 2)}%`;
34
+ }
35
+ const UsagePage = () => {
36
+ const auth = useAuth();
37
+ const [range, setRange] = useState("30d");
38
+ const { data, isLoading, isError } = useUsage(range);
39
+ const maxBar = Math.max(1, ...data?.series.map((point) => point.requestCount) ?? [1]);
40
+ const endpointTotal = data?.byEndpoint.reduce((sum, item) => sum + item.count, 0) ?? 0;
41
+ if (!auth.isAuthenticated) return /* @__PURE__ */ jsxs("div", {
42
+ className: "px-8 py-7",
43
+ children: [
44
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
45
+ /* @__PURE__ */ jsx("h1", {
46
+ className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
47
+ children: "Usage"
48
+ }),
49
+ /* @__PURE__ */ jsx("p", {
50
+ className: "text-muted-foreground",
51
+ children: "Sign in to view your API usage."
52
+ }),
53
+ auth.isAuthEnabled ? /* @__PURE__ */ jsx(Button$1, {
54
+ asChild: true,
55
+ className: "mt-4",
56
+ children: /* @__PURE__ */ jsx(Link$1, {
57
+ to: "/signin",
58
+ children: "Sign in"
59
+ })
60
+ }) : null
61
+ ]
62
+ });
63
+ return /* @__PURE__ */ jsxs("div", {
64
+ className: "px-8 py-7",
65
+ children: [
66
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
67
+ /* @__PURE__ */ jsxs("div", {
68
+ className: "mb-6 flex flex-wrap items-center justify-between gap-3",
69
+ children: [/* @__PURE__ */ jsx("h1", {
70
+ className: "text-[1.6rem] font-bold tracking-[-0.01em]",
71
+ children: "Usage"
72
+ }), /* @__PURE__ */ jsx("div", {
73
+ className: "flex gap-1 rounded-lg border border-border bg-card p-1",
74
+ children: RANGES.map((item) => /* @__PURE__ */ jsx("button", {
75
+ type: "button",
76
+ onClick: () => setRange(item.value),
77
+ className: `rounded-md px-3 py-1.5 text-xs font-semibold transition-colors ${range === item.value ? "bg-primary/14 text-primary" : "text-muted-foreground hover:text-foreground"}`,
78
+ children: item.label
79
+ }, item.value))
80
+ })]
81
+ }),
82
+ isLoading ? /* @__PURE__ */ jsx("p", {
83
+ className: "text-sm text-muted-foreground",
84
+ children: "Loading usage…"
85
+ }) : null,
86
+ isError ? /* @__PURE__ */ jsx("p", {
87
+ className: "text-sm text-destructive",
88
+ children: "Unable to load usage data."
89
+ }) : null,
90
+ /* @__PURE__ */ jsxs("div", {
91
+ className: "mb-5 grid grid-cols-1 gap-4 sm:grid-cols-3",
92
+ children: [
93
+ /* @__PURE__ */ jsxs("div", {
94
+ className: "rounded-xl border border-border bg-card p-4",
95
+ children: [/* @__PURE__ */ jsx("div", {
96
+ className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
97
+ children: "Requests"
98
+ }), /* @__PURE__ */ jsx("div", {
99
+ className: "mt-1.5 text-2xl font-bold",
100
+ children: data ? formatNumber(data.requestCount) : "—"
101
+ })]
102
+ }),
103
+ /* @__PURE__ */ jsxs("div", {
104
+ className: "rounded-xl border border-border bg-card p-4",
105
+ children: [/* @__PURE__ */ jsx("div", {
106
+ className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
107
+ children: "Avg latency"
108
+ }), /* @__PURE__ */ jsx("div", {
109
+ className: "mt-1.5 text-2xl font-bold",
110
+ children: data?.avgLatencyMs != null ? `${data.avgLatencyMs} ms` : "—"
111
+ })]
112
+ }),
113
+ /* @__PURE__ */ jsxs("div", {
114
+ className: "rounded-xl border border-border bg-card p-4",
115
+ children: [/* @__PURE__ */ jsx("div", {
116
+ className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
117
+ children: "Error rate"
118
+ }), /* @__PURE__ */ jsx("div", {
119
+ className: "mt-1.5 text-2xl font-bold",
120
+ children: data ? formatPct(data.errorRate) : "—"
121
+ })]
122
+ })
123
+ ]
124
+ }),
125
+ /* @__PURE__ */ jsxs("div", {
126
+ className: "mb-5 rounded-xl border border-border bg-card p-6",
127
+ children: [/* @__PURE__ */ jsxs("h2", {
128
+ className: "mb-4 text-lg font-semibold",
129
+ children: [
130
+ "Requests over time (",
131
+ range,
132
+ ")"
133
+ ]
134
+ }), data && data.series.some((p) => p.requestCount > 0) ? /* @__PURE__ */ jsx("div", {
135
+ className: "flex h-40 items-end gap-1.5",
136
+ children: data.series.map((point) => /* @__PURE__ */ jsx("div", {
137
+ title: `${point.dayUtc}: ${point.requestCount}`,
138
+ className: "min-w-0 flex-1 rounded-t-sm bg-primary",
139
+ style: { height: `${Math.max(4, point.requestCount / maxBar * 100)}%` }
140
+ }, point.dayUtc))
141
+ }) : /* @__PURE__ */ jsx("p", {
142
+ className: "text-sm text-muted-foreground",
143
+ children: "No request data for this range yet."
144
+ })]
145
+ }),
146
+ /* @__PURE__ */ jsxs("div", {
147
+ className: "grid grid-cols-1 gap-5 lg:grid-cols-[1.2fr_1fr]",
148
+ children: [/* @__PURE__ */ jsxs("div", {
149
+ className: "rounded-xl border border-border bg-card p-6",
150
+ children: [/* @__PURE__ */ jsx("h2", {
151
+ className: "mb-4 text-lg font-semibold",
152
+ children: "By endpoint"
153
+ }), data && data.byEndpoint.length > 0 ? /* @__PURE__ */ jsx("div", {
154
+ className: "space-y-3.5",
155
+ children: data.byEndpoint.map((item) => {
156
+ const pct = endpointTotal > 0 ? Math.round(item.count / endpointTotal * 100) : 0;
157
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
158
+ className: "mb-1 flex justify-between text-[13px]",
159
+ children: [/* @__PURE__ */ jsx("code", {
160
+ className: "font-mono text-foreground",
161
+ children: item.path
162
+ }), /* @__PURE__ */ jsxs("span", {
163
+ className: "text-muted-foreground",
164
+ children: [pct, "%"]
165
+ })]
166
+ }), /* @__PURE__ */ jsx("div", {
167
+ className: "h-1.5 rounded-full bg-muted",
168
+ children: /* @__PURE__ */ jsx("div", {
169
+ className: "h-full rounded-full bg-primary",
170
+ style: { width: `${pct}%` }
171
+ })
172
+ })] }, item.path);
173
+ })
174
+ }) : /* @__PURE__ */ jsx("p", {
175
+ className: "text-sm text-muted-foreground",
176
+ children: "No endpoint data."
177
+ })]
178
+ }), /* @__PURE__ */ jsxs("div", {
179
+ className: "rounded-xl border border-border bg-card p-6",
180
+ children: [/* @__PURE__ */ jsx("h2", {
181
+ className: "mb-4 text-lg font-semibold",
182
+ children: "Status codes"
183
+ }), data && data.byStatus.length > 0 ? /* @__PURE__ */ jsx("div", { children: data.byStatus.map((item) => /* @__PURE__ */ jsxs("div", {
184
+ className: "flex items-center justify-between border-b border-border py-2.5 last:border-0",
185
+ children: [/* @__PURE__ */ jsx("span", {
186
+ className: "text-sm font-medium",
187
+ children: item.label
188
+ }), /* @__PURE__ */ jsx("span", {
189
+ className: "text-sm font-bold",
190
+ children: formatNumber(item.count)
191
+ })]
192
+ }, item.label)) }) : /* @__PURE__ */ jsx("p", {
193
+ className: "text-sm text-muted-foreground",
194
+ children: "No status data."
195
+ })]
196
+ })]
197
+ })
198
+ ]
199
+ });
200
+ };
201
+
202
+ //#endregion
203
+ export { UsagePage as default };