@nordsym/apiclaw 1.0.0 → 1.1.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/AGENTS.md +74 -0
- package/HEARTBEAT.md +4 -0
- package/IDENTITY.md +22 -0
- package/README.md +193 -202
- package/SOUL.md +36 -0
- package/STATUS.md +237 -0
- package/TOOLS.md +36 -0
- package/USER.md +17 -0
- package/{backend/convex → convex}/_generated/api.d.ts +12 -6
- package/convex/analytics.ts +90 -0
- package/convex/credits.ts +211 -0
- package/convex/http.ts +578 -0
- package/convex/providers.ts +516 -0
- package/convex/purchases.ts +183 -0
- package/convex/ratelimit.ts +104 -0
- package/convex/schema.ts +220 -0
- package/convex/telemetry.ts +81 -0
- package/convex.json +3 -0
- package/dist/credentials.d.ts +19 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +158 -0
- package/dist/credentials.js.map +1 -0
- package/dist/credits.d.ts +14 -11
- package/dist/credits.d.ts.map +1 -1
- package/dist/credits.js +151 -99
- package/dist/credits.js.map +1 -1
- package/dist/discovery.d.ts +7 -16
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +33 -40
- package/dist/discovery.js.map +1 -1
- package/dist/execute.d.ts +19 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +285 -0
- package/dist/execute.js.map +1 -0
- package/dist/index.js +175 -31
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +19 -0
- package/dist/proxy.js.map +1 -0
- package/dist/registry/apis.json +95362 -202
- package/dist/registry/apis_expanded.json +100853 -0
- package/dist/stripe.d.ts +68 -0
- package/dist/stripe.d.ts.map +1 -0
- package/dist/stripe.js +196 -0
- package/dist/stripe.js.map +1 -0
- package/dist/telemetry.d.ts +28 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +50 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/test.d.ts +3 -2
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +105 -75
- package/dist/test.js.map +1 -1
- package/dist/types.d.ts +0 -28
- package/dist/types.d.ts.map +1 -1
- package/dist/webhook.d.ts +2 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +90 -0
- package/dist/webhook.js.map +1 -0
- package/landing/DESIGN.md +343 -0
- package/landing/package-lock.json +1196 -7
- package/landing/package.json +5 -1
- package/landing/public/android-chrome-192x192.png +0 -0
- package/landing/public/android-chrome-512x512.png +0 -0
- package/landing/public/apple-touch-icon.png +0 -0
- package/landing/public/demo.gif +0 -0
- package/landing/public/demo.mp4 +0 -0
- package/landing/public/favicon-16x16.png +0 -0
- package/landing/public/favicon-32x32.png +0 -0
- package/landing/public/favicon.ico +0 -0
- package/landing/public/favicon.svg +3 -0
- package/landing/public/icon.svg +47 -0
- package/landing/public/logo-mono.svg +37 -0
- package/landing/public/logo-simple.svg +45 -0
- package/landing/public/logo.svg +84 -0
- package/landing/public/og-template.html +184 -0
- package/landing/public/site.webmanifest +31 -0
- package/landing/scripts/generate-assets.js +284 -0
- package/landing/scripts/generate-pngs.js +48 -0
- package/landing/scripts/generate-stats.js +42 -0
- package/landing/src/app/admin/page.tsx +348 -0
- package/landing/src/app/api/auth/magic-link/route.ts +73 -0
- package/landing/src/app/api/auth/session/route.ts +38 -0
- package/landing/src/app/api/auth/verify/route.ts +43 -0
- package/landing/src/app/api/og/route.tsx +84 -0
- package/landing/src/app/globals.css +439 -100
- package/landing/src/app/layout.tsx +37 -7
- package/landing/src/app/page.tsx +627 -552
- package/landing/src/app/providers/dashboard/login/page.tsx +176 -0
- package/landing/src/app/providers/dashboard/page.tsx +589 -0
- package/landing/src/app/providers/dashboard/verify/page.tsx +106 -0
- package/landing/src/app/providers/layout.tsx +14 -0
- package/landing/src/app/providers/page.tsx +402 -0
- package/landing/src/app/providers/register/page.tsx +670 -0
- package/landing/src/components/ProviderDashboard.tsx +794 -0
- package/landing/src/hooks/useDashboardData.ts +99 -0
- package/landing/src/lib/apis.json +116054 -0
- package/landing/src/lib/convex-client.ts +106 -0
- package/landing/src/lib/mock-data.ts +285 -0
- package/landing/src/lib/stats.json +6 -0
- package/landing/tailwind.config.ts +12 -11
- package/landing/tsconfig.tsbuildinfo +1 -0
- package/package.json +21 -20
- package/scripts/SYMBOT-FIX.md +238 -0
- package/scripts/demo-simulation.py +177 -0
- package/scripts/expand-more.py +502 -0
- package/scripts/expand-registry.py +434 -0
- package/scripts/history-sanitizer.ts +272 -0
- package/scripts/mass-scrape.py +1308 -0
- package/scripts/sync-and-deploy.sh +36 -0
- package/src/credentials.ts +177 -0
- package/src/credits.ts +190 -122
- package/src/discovery.ts +45 -58
- package/src/execute.ts +350 -0
- package/src/index.ts +184 -32
- package/src/proxy.ts +24 -0
- package/src/registry/apis.json +95362 -202
- package/src/registry/apis_expanded.json +100853 -0
- package/src/stripe.ts +243 -0
- package/src/telemetry.ts +71 -0
- package/src/test.ts +127 -89
- package/src/types.ts +0 -34
- package/src/webhook.ts +107 -0
- package/.github/ISSUE_TEMPLATE/add-api.yml +0 -123
- package/BRIEFING.md +0 -30
- package/backend/convex/apiKeys.ts +0 -75
- package/backend/convex/purchases.ts +0 -74
- package/backend/convex/schema.ts +0 -45
- package/backend/convex/transactions.ts +0 -57
- package/backend/convex/users.ts +0 -94
- package/backend/package-lock.json +0 -521
- package/backend/package.json +0 -15
- package/dist/registry/parse_apis.py +0 -146
- package/dist/revenuecat.d.ts +0 -61
- package/dist/revenuecat.d.ts.map +0 -1
- package/dist/revenuecat.js +0 -166
- package/dist/revenuecat.js.map +0 -1
- package/dist/webhooks/revenuecat.d.ts +0 -48
- package/dist/webhooks/revenuecat.d.ts.map +0 -1
- package/dist/webhooks/revenuecat.js +0 -119
- package/dist/webhooks/revenuecat.js.map +0 -1
- package/docs/revenuecat-setup.md +0 -89
- package/landing/src/app/api/keys/route.ts +0 -71
- package/landing/src/app/api/log/route.ts +0 -37
- package/landing/src/app/api/stats/route.ts +0 -37
- package/landing/src/app/page.tsx.bak +0 -567
- package/landing/src/components/AddKeyModal.tsx +0 -159
- package/newsletter-template.html +0 -71
- package/outreach/OUTREACH-SYSTEM.md +0 -211
- package/outreach/email-template.html +0 -179
- package/outreach/targets.md +0 -133
- package/src/registry/parse_apis.py +0 -146
- package/src/revenuecat.ts +0 -239
- package/src/webhooks/revenuecat.ts +0 -187
- /package/{backend/convex → convex}/README.md +0 -0
- /package/{backend/convex → convex}/_generated/api.js +0 -0
- /package/{backend/convex → convex}/_generated/dataModel.d.ts +0 -0
- /package/{backend/convex → convex}/_generated/server.d.ts +0 -0
- /package/{backend/convex → convex}/_generated/server.js +0 -0
- /package/{backend/convex → convex}/tsconfig.json +0 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
BarChart3,
|
|
6
|
+
CreditCard,
|
|
7
|
+
Settings,
|
|
8
|
+
TrendingUp,
|
|
9
|
+
Users,
|
|
10
|
+
Zap,
|
|
11
|
+
DollarSign,
|
|
12
|
+
ArrowUpRight,
|
|
13
|
+
ArrowDownRight,
|
|
14
|
+
Eye,
|
|
15
|
+
EyeOff,
|
|
16
|
+
Copy,
|
|
17
|
+
RefreshCw,
|
|
18
|
+
Pause,
|
|
19
|
+
Play,
|
|
20
|
+
ExternalLink,
|
|
21
|
+
ChevronRight,
|
|
22
|
+
Check,
|
|
23
|
+
Clock,
|
|
24
|
+
AlertCircle,
|
|
25
|
+
} from "lucide-react";
|
|
26
|
+
import {
|
|
27
|
+
LineChart,
|
|
28
|
+
Line,
|
|
29
|
+
BarChart,
|
|
30
|
+
Bar,
|
|
31
|
+
XAxis,
|
|
32
|
+
YAxis,
|
|
33
|
+
CartesianGrid,
|
|
34
|
+
Tooltip,
|
|
35
|
+
ResponsiveContainer,
|
|
36
|
+
PieChart,
|
|
37
|
+
Pie,
|
|
38
|
+
Cell,
|
|
39
|
+
} from "recharts";
|
|
40
|
+
import {
|
|
41
|
+
getMockProvider,
|
|
42
|
+
getMockApis,
|
|
43
|
+
getMockAnalytics,
|
|
44
|
+
getMockEarnings,
|
|
45
|
+
getMockCredentials,
|
|
46
|
+
type Api,
|
|
47
|
+
} from "@/lib/mock-data";
|
|
48
|
+
|
|
49
|
+
type TabType = "overview" | "apis" | "earnings";
|
|
50
|
+
|
|
51
|
+
const COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6"];
|
|
52
|
+
|
|
53
|
+
export function ProviderDashboard() {
|
|
54
|
+
const [activeTab, setActiveTab] = useState<TabType>("overview");
|
|
55
|
+
const [period, setPeriod] = useState<"week" | "month" | "all">("month");
|
|
56
|
+
const [selectedApi, setSelectedApi] = useState<string | null>(null);
|
|
57
|
+
|
|
58
|
+
const provider = getMockProvider();
|
|
59
|
+
const apis = getMockApis();
|
|
60
|
+
const analytics = getMockAnalytics();
|
|
61
|
+
const earnings = getMockEarnings();
|
|
62
|
+
|
|
63
|
+
const tabs = [
|
|
64
|
+
{ id: "overview" as TabType, label: "Overview", icon: BarChart3 },
|
|
65
|
+
{ id: "apis" as TabType, label: "APIs", icon: Zap },
|
|
66
|
+
{ id: "earnings" as TabType, label: "Earnings", icon: CreditCard },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="min-h-screen bg-background">
|
|
71
|
+
{/* Header */}
|
|
72
|
+
<header className="border-b border-border bg-surface/50 backdrop-blur-xl sticky top-0 z-50">
|
|
73
|
+
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
74
|
+
<div className="flex items-center gap-4">
|
|
75
|
+
<div className="w-10 h-10 rounded-xl bg-accent/20 flex items-center justify-center text-xl">
|
|
76
|
+
🦞
|
|
77
|
+
</div>
|
|
78
|
+
<div>
|
|
79
|
+
<h1 className="font-bold text-lg">Provider Dashboard</h1>
|
|
80
|
+
<p className="text-sm text-text-muted">{provider.company || provider.email}</p>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="flex items-center gap-3">
|
|
84
|
+
<button className="p-2 rounded-lg hover:bg-surface transition">
|
|
85
|
+
<Settings className="w-5 h-5 text-text-muted" />
|
|
86
|
+
</button>
|
|
87
|
+
<div className="w-9 h-9 rounded-full bg-accent/20 flex items-center justify-center text-sm font-medium">
|
|
88
|
+
{provider.name.charAt(0).toUpperCase()}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</header>
|
|
93
|
+
|
|
94
|
+
<div className="max-w-7xl mx-auto px-6 py-8">
|
|
95
|
+
{/* Tab Navigation */}
|
|
96
|
+
<div className="flex items-center gap-1 p-1 bg-surface rounded-xl w-fit mb-8">
|
|
97
|
+
{tabs.map((tab) => (
|
|
98
|
+
<button
|
|
99
|
+
key={tab.id}
|
|
100
|
+
onClick={() => setActiveTab(tab.id)}
|
|
101
|
+
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
|
102
|
+
activeTab === tab.id
|
|
103
|
+
? "bg-accent text-white"
|
|
104
|
+
: "text-text-muted hover:text-text-primary"
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
<tab.icon className="w-4 h-4" />
|
|
108
|
+
{tab.label}
|
|
109
|
+
</button>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* Tab Content */}
|
|
114
|
+
{activeTab === "overview" && (
|
|
115
|
+
<OverviewTab
|
|
116
|
+
analytics={analytics}
|
|
117
|
+
period={period}
|
|
118
|
+
setPeriod={setPeriod}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
{activeTab === "apis" && (
|
|
122
|
+
<ApisTab
|
|
123
|
+
apis={apis}
|
|
124
|
+
selectedApi={selectedApi}
|
|
125
|
+
setSelectedApi={setSelectedApi}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
{activeTab === "earnings" && <EarningsTab earnings={earnings} />}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============================================
|
|
135
|
+
// OVERVIEW TAB
|
|
136
|
+
// ============================================
|
|
137
|
+
|
|
138
|
+
function OverviewTab({
|
|
139
|
+
analytics,
|
|
140
|
+
period,
|
|
141
|
+
setPeriod,
|
|
142
|
+
}: {
|
|
143
|
+
analytics: ReturnType<typeof getMockAnalytics>;
|
|
144
|
+
period: "week" | "month" | "all";
|
|
145
|
+
setPeriod: (p: "week" | "month" | "all") => void;
|
|
146
|
+
}) {
|
|
147
|
+
const periodData = {
|
|
148
|
+
week: {
|
|
149
|
+
calls: analytics.totalCallsWeek,
|
|
150
|
+
revenue: analytics.revenueWeek,
|
|
151
|
+
chartData: analytics.callsByDay.slice(-7),
|
|
152
|
+
},
|
|
153
|
+
month: {
|
|
154
|
+
calls: analytics.totalCallsMonth,
|
|
155
|
+
revenue: analytics.revenueMonth,
|
|
156
|
+
chartData: analytics.callsByDay.slice(-30),
|
|
157
|
+
},
|
|
158
|
+
all: {
|
|
159
|
+
calls: analytics.totalCalls,
|
|
160
|
+
revenue: analytics.totalRevenue,
|
|
161
|
+
chartData: analytics.callsByDay,
|
|
162
|
+
},
|
|
163
|
+
}[period];
|
|
164
|
+
|
|
165
|
+
// Calculate growth
|
|
166
|
+
const prevPeriodCalls =
|
|
167
|
+
period === "week"
|
|
168
|
+
? analytics.callsByDay.slice(-14, -7).reduce((s, d) => s + d.calls, 0)
|
|
169
|
+
: period === "month"
|
|
170
|
+
? analytics.callsByDay.slice(-60, -30).reduce((s, d) => s + d.calls, 0)
|
|
171
|
+
: 0;
|
|
172
|
+
const callsGrowth =
|
|
173
|
+
prevPeriodCalls > 0
|
|
174
|
+
? ((periodData.calls - prevPeriodCalls) / prevPeriodCalls) * 100
|
|
175
|
+
: 0;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="space-y-8">
|
|
179
|
+
{/* Period Selector */}
|
|
180
|
+
<div className="flex items-center justify-between">
|
|
181
|
+
<h2 className="text-2xl font-bold">Analytics</h2>
|
|
182
|
+
<div className="flex items-center gap-1 p-1 bg-surface rounded-lg">
|
|
183
|
+
{(["week", "month", "all"] as const).map((p) => (
|
|
184
|
+
<button
|
|
185
|
+
key={p}
|
|
186
|
+
onClick={() => setPeriod(p)}
|
|
187
|
+
className={`px-3 py-1.5 rounded-md text-sm font-medium transition ${
|
|
188
|
+
period === p
|
|
189
|
+
? "bg-surface-elevated text-text-primary"
|
|
190
|
+
: "text-text-muted hover:text-text-primary"
|
|
191
|
+
}`}
|
|
192
|
+
>
|
|
193
|
+
{p === "week" ? "7 days" : p === "month" ? "30 days" : "All time"}
|
|
194
|
+
</button>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Stats Grid */}
|
|
200
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
201
|
+
<StatCard
|
|
202
|
+
title="Total Calls"
|
|
203
|
+
value={periodData.calls.toLocaleString()}
|
|
204
|
+
change={callsGrowth}
|
|
205
|
+
icon={Zap}
|
|
206
|
+
/>
|
|
207
|
+
<StatCard
|
|
208
|
+
title="Unique Agents"
|
|
209
|
+
value={analytics.uniqueAgents.toString()}
|
|
210
|
+
icon={Users}
|
|
211
|
+
/>
|
|
212
|
+
<StatCard
|
|
213
|
+
title="Revenue"
|
|
214
|
+
value={`$${periodData.revenue.toFixed(2)}`}
|
|
215
|
+
icon={DollarSign}
|
|
216
|
+
accent
|
|
217
|
+
/>
|
|
218
|
+
<StatCard
|
|
219
|
+
title="Avg Calls/Day"
|
|
220
|
+
value={Math.round(periodData.calls / periodData.chartData.length).toString()}
|
|
221
|
+
icon={TrendingUp}
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* Charts */}
|
|
226
|
+
<div className="grid lg:grid-cols-3 gap-6">
|
|
227
|
+
{/* Line Chart - Calls Over Time */}
|
|
228
|
+
<div className="lg:col-span-2 bg-surface-elevated rounded-2xl border border-border p-6">
|
|
229
|
+
<h3 className="font-semibold mb-4">Calls Over Time</h3>
|
|
230
|
+
<div className="h-80">
|
|
231
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
232
|
+
<LineChart data={periodData.chartData}>
|
|
233
|
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
|
234
|
+
<XAxis
|
|
235
|
+
dataKey="date"
|
|
236
|
+
tick={{ fontSize: 12, fill: "var(--text-muted)" }}
|
|
237
|
+
tickFormatter={(d) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
|
|
238
|
+
/>
|
|
239
|
+
<YAxis tick={{ fontSize: 12, fill: "var(--text-muted)" }} />
|
|
240
|
+
<Tooltip
|
|
241
|
+
contentStyle={{
|
|
242
|
+
background: "var(--surface-elevated)",
|
|
243
|
+
border: "1px solid var(--border)",
|
|
244
|
+
borderRadius: "8px",
|
|
245
|
+
}}
|
|
246
|
+
labelFormatter={(d) => new Date(d).toLocaleDateString()}
|
|
247
|
+
/>
|
|
248
|
+
<Line
|
|
249
|
+
type="monotone"
|
|
250
|
+
dataKey="calls"
|
|
251
|
+
stroke="#ef4444"
|
|
252
|
+
strokeWidth={2}
|
|
253
|
+
dot={false}
|
|
254
|
+
activeDot={{ r: 4, fill: "#ef4444" }}
|
|
255
|
+
/>
|
|
256
|
+
</LineChart>
|
|
257
|
+
</ResponsiveContainer>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{/* Top Agents */}
|
|
262
|
+
<div className="bg-surface-elevated rounded-2xl border border-border p-6">
|
|
263
|
+
<h3 className="font-semibold mb-4">Top Agents</h3>
|
|
264
|
+
<div className="space-y-3">
|
|
265
|
+
{analytics.topAgents.slice(0, 6).map((agent, i) => (
|
|
266
|
+
<div key={agent.agentId} className="flex items-center justify-between">
|
|
267
|
+
<div className="flex items-center gap-3">
|
|
268
|
+
<span className="w-6 h-6 rounded-full bg-surface flex items-center justify-center text-xs font-medium text-text-muted">
|
|
269
|
+
{i + 1}
|
|
270
|
+
</span>
|
|
271
|
+
<span className="text-sm font-mono truncate max-w-[140px]">
|
|
272
|
+
{agent.agentId.replace("agent_", "")}
|
|
273
|
+
</span>
|
|
274
|
+
</div>
|
|
275
|
+
<span className="text-sm text-text-muted">{agent.calls.toLocaleString()}</span>
|
|
276
|
+
</div>
|
|
277
|
+
))}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{/* Second Row - Bar Chart & Pie Chart */}
|
|
283
|
+
<div className="grid lg:grid-cols-2 gap-6">
|
|
284
|
+
{/* Calls per API */}
|
|
285
|
+
<div className="bg-surface-elevated rounded-2xl border border-border p-6">
|
|
286
|
+
<h3 className="font-semibold mb-4">Calls per API</h3>
|
|
287
|
+
<div className="h-64">
|
|
288
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
289
|
+
<BarChart data={analytics.apis} layout="vertical">
|
|
290
|
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
|
291
|
+
<XAxis type="number" tick={{ fontSize: 12, fill: "var(--text-muted)" }} />
|
|
292
|
+
<YAxis
|
|
293
|
+
type="category"
|
|
294
|
+
dataKey="name"
|
|
295
|
+
tick={{ fontSize: 12, fill: "var(--text-muted)" }}
|
|
296
|
+
width={120}
|
|
297
|
+
/>
|
|
298
|
+
<Tooltip
|
|
299
|
+
contentStyle={{
|
|
300
|
+
background: "var(--surface-elevated)",
|
|
301
|
+
border: "1px solid var(--border)",
|
|
302
|
+
borderRadius: "8px",
|
|
303
|
+
}}
|
|
304
|
+
/>
|
|
305
|
+
<Bar dataKey="calls" fill="#ef4444" radius={[0, 4, 4, 0]} />
|
|
306
|
+
</BarChart>
|
|
307
|
+
</ResponsiveContainer>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{/* Geographic Distribution */}
|
|
312
|
+
<div className="bg-surface-elevated rounded-2xl border border-border p-6">
|
|
313
|
+
<h3 className="font-semibold mb-4">Geographic Distribution</h3>
|
|
314
|
+
<div className="h-64 flex items-center justify-center">
|
|
315
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
316
|
+
<PieChart>
|
|
317
|
+
<Pie
|
|
318
|
+
data={Object.entries(analytics.callsByRegion).map(([name, value]) => ({
|
|
319
|
+
name,
|
|
320
|
+
value,
|
|
321
|
+
}))}
|
|
322
|
+
cx="50%"
|
|
323
|
+
cy="50%"
|
|
324
|
+
innerRadius={60}
|
|
325
|
+
outerRadius={90}
|
|
326
|
+
paddingAngle={2}
|
|
327
|
+
dataKey="value"
|
|
328
|
+
>
|
|
329
|
+
{Object.keys(analytics.callsByRegion).map((_, index) => (
|
|
330
|
+
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
331
|
+
))}
|
|
332
|
+
</Pie>
|
|
333
|
+
<Tooltip
|
|
334
|
+
contentStyle={{
|
|
335
|
+
background: "var(--surface-elevated)",
|
|
336
|
+
border: "1px solid var(--border)",
|
|
337
|
+
borderRadius: "8px",
|
|
338
|
+
}}
|
|
339
|
+
/>
|
|
340
|
+
</PieChart>
|
|
341
|
+
</ResponsiveContainer>
|
|
342
|
+
</div>
|
|
343
|
+
<div className="flex flex-wrap justify-center gap-4 mt-4">
|
|
344
|
+
{Object.entries(analytics.callsByRegion).map(([region, calls], i) => (
|
|
345
|
+
<div key={region} className="flex items-center gap-2">
|
|
346
|
+
<div
|
|
347
|
+
className="w-3 h-3 rounded-full"
|
|
348
|
+
style={{ background: COLORS[i % COLORS.length] }}
|
|
349
|
+
/>
|
|
350
|
+
<span className="text-sm text-text-muted">
|
|
351
|
+
{region}: {calls.toLocaleString()}
|
|
352
|
+
</span>
|
|
353
|
+
</div>
|
|
354
|
+
))}
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function StatCard({
|
|
363
|
+
title,
|
|
364
|
+
value,
|
|
365
|
+
change,
|
|
366
|
+
icon: Icon,
|
|
367
|
+
accent,
|
|
368
|
+
}: {
|
|
369
|
+
title: string;
|
|
370
|
+
value: string;
|
|
371
|
+
change?: number;
|
|
372
|
+
icon: typeof Zap;
|
|
373
|
+
accent?: boolean;
|
|
374
|
+
}) {
|
|
375
|
+
return (
|
|
376
|
+
<div className={`rounded-2xl border p-5 ${accent ? "bg-accent/10 border-accent/30" : "bg-surface-elevated border-border"}`}>
|
|
377
|
+
<div className="flex items-center justify-between mb-3">
|
|
378
|
+
<span className="text-sm text-text-muted">{title}</span>
|
|
379
|
+
<Icon className={`w-5 h-5 ${accent ? "text-accent" : "text-text-muted"}`} />
|
|
380
|
+
</div>
|
|
381
|
+
<div className="flex items-end justify-between">
|
|
382
|
+
<span className={`text-3xl font-bold ${accent ? "text-accent" : ""}`}>{value}</span>
|
|
383
|
+
{change !== undefined && (
|
|
384
|
+
<div className={`flex items-center gap-1 text-sm ${change >= 0 ? "text-green-500" : "text-red-500"}`}>
|
|
385
|
+
{change >= 0 ? <ArrowUpRight className="w-4 h-4" /> : <ArrowDownRight className="w-4 h-4" />}
|
|
386
|
+
{Math.abs(change).toFixed(1)}%
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ============================================
|
|
395
|
+
// APIS TAB
|
|
396
|
+
// ============================================
|
|
397
|
+
|
|
398
|
+
function ApisTab({
|
|
399
|
+
apis,
|
|
400
|
+
selectedApi,
|
|
401
|
+
setSelectedApi,
|
|
402
|
+
}: {
|
|
403
|
+
apis: Api[];
|
|
404
|
+
selectedApi: string | null;
|
|
405
|
+
setSelectedApi: (id: string | null) => void;
|
|
406
|
+
}) {
|
|
407
|
+
const selected = apis.find((a) => a.id === selectedApi);
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<div className="space-y-6">
|
|
411
|
+
<div className="flex items-center justify-between">
|
|
412
|
+
<h2 className="text-2xl font-bold">Your APIs</h2>
|
|
413
|
+
<button className="btn-primary !py-2 !px-4 text-sm">
|
|
414
|
+
<Zap className="w-4 h-4" />
|
|
415
|
+
Add API
|
|
416
|
+
</button>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<div className="grid lg:grid-cols-3 gap-6">
|
|
420
|
+
{/* API List */}
|
|
421
|
+
<div className="lg:col-span-1 space-y-3">
|
|
422
|
+
{apis.map((api) => (
|
|
423
|
+
<button
|
|
424
|
+
key={api.id}
|
|
425
|
+
onClick={() => setSelectedApi(api.id)}
|
|
426
|
+
className={`w-full text-left rounded-xl border p-4 transition-all ${
|
|
427
|
+
selectedApi === api.id
|
|
428
|
+
? "bg-accent/10 border-accent"
|
|
429
|
+
: "bg-surface-elevated border-border hover:border-accent/50"
|
|
430
|
+
}`}
|
|
431
|
+
>
|
|
432
|
+
<div className="flex items-center justify-between mb-2">
|
|
433
|
+
<div className="flex items-center gap-2">
|
|
434
|
+
<span className="text-2xl">{api.icon}</span>
|
|
435
|
+
<span className="font-semibold">{api.name}</span>
|
|
436
|
+
</div>
|
|
437
|
+
<span
|
|
438
|
+
className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
439
|
+
api.status === "active"
|
|
440
|
+
? "bg-green-500/20 text-green-500"
|
|
441
|
+
: "bg-yellow-500/20 text-yellow-600"
|
|
442
|
+
}`}
|
|
443
|
+
>
|
|
444
|
+
{api.status}
|
|
445
|
+
</span>
|
|
446
|
+
</div>
|
|
447
|
+
<p className="text-sm text-text-muted line-clamp-2">{api.description}</p>
|
|
448
|
+
<div className="flex items-center gap-4 mt-3 text-sm text-text-muted">
|
|
449
|
+
<span>{api.calls.toLocaleString()} calls</span>
|
|
450
|
+
<span>{api.category}</span>
|
|
451
|
+
</div>
|
|
452
|
+
</button>
|
|
453
|
+
))}
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
{/* API Details */}
|
|
457
|
+
<div className="lg:col-span-2">
|
|
458
|
+
{selected ? (
|
|
459
|
+
<ApiDetails api={selected} />
|
|
460
|
+
) : (
|
|
461
|
+
<div className="h-full flex items-center justify-center rounded-2xl border border-dashed border-border bg-surface/50 p-12">
|
|
462
|
+
<p className="text-text-muted">Select an API to view details</p>
|
|
463
|
+
</div>
|
|
464
|
+
)}
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function ApiDetails({ api }: { api: Api }) {
|
|
472
|
+
const [showSecret, setShowSecret] = useState(false);
|
|
473
|
+
const [copied, setCopied] = useState<string | null>(null);
|
|
474
|
+
const credentials = getMockCredentials(api.id);
|
|
475
|
+
|
|
476
|
+
const copyToClipboard = (text: string, key: string) => {
|
|
477
|
+
navigator.clipboard.writeText(text);
|
|
478
|
+
setCopied(key);
|
|
479
|
+
setTimeout(() => setCopied(null), 2000);
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<div className="bg-surface-elevated rounded-2xl border border-border">
|
|
484
|
+
{/* Header */}
|
|
485
|
+
<div className="p-6 border-b border-border">
|
|
486
|
+
<div className="flex items-center justify-between mb-4">
|
|
487
|
+
<div className="flex items-center gap-3">
|
|
488
|
+
<span className="text-4xl">{api.icon}</span>
|
|
489
|
+
<div>
|
|
490
|
+
<h3 className="text-xl font-bold">{api.name}</h3>
|
|
491
|
+
<p className="text-sm text-text-muted">{api.category}</p>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
<div className="flex items-center gap-2">
|
|
495
|
+
<button className="btn-secondary !py-2 !px-3 text-sm">
|
|
496
|
+
{api.status === "active" ? (
|
|
497
|
+
<>
|
|
498
|
+
<Pause className="w-4 h-4" /> Pause
|
|
499
|
+
</>
|
|
500
|
+
) : (
|
|
501
|
+
<>
|
|
502
|
+
<Play className="w-4 h-4" /> Resume
|
|
503
|
+
</>
|
|
504
|
+
)}
|
|
505
|
+
</button>
|
|
506
|
+
<button className="btn-secondary !py-2 !px-3 text-sm">
|
|
507
|
+
<Settings className="w-4 h-4" /> Edit
|
|
508
|
+
</button>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
<p className="text-text-secondary">{api.description}</p>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
{/* Details Grid */}
|
|
515
|
+
<div className="grid md:grid-cols-2 gap-6 p-6 border-b border-border">
|
|
516
|
+
<div className="space-y-4">
|
|
517
|
+
<h4 className="font-semibold text-sm text-text-muted uppercase tracking-wider">Configuration</h4>
|
|
518
|
+
<div className="space-y-3">
|
|
519
|
+
<DetailRow label="Base URL" value={api.baseUrl} mono />
|
|
520
|
+
<DetailRow
|
|
521
|
+
label="Documentation"
|
|
522
|
+
value={
|
|
523
|
+
api.docsUrl ? (
|
|
524
|
+
<a href={api.docsUrl} target="_blank" rel="noopener noreferrer" className="text-accent hover:underline flex items-center gap-1">
|
|
525
|
+
View Docs <ExternalLink className="w-3 h-3" />
|
|
526
|
+
</a>
|
|
527
|
+
) : (
|
|
528
|
+
"Not set"
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
/>
|
|
532
|
+
<DetailRow label="Auth Type" value={api.authType} />
|
|
533
|
+
<DetailRow label="Rate Limit" value={`${api.rateLimitPerMinute || "∞"}/min`} />
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
<div className="space-y-4">
|
|
537
|
+
<h4 className="font-semibold text-sm text-text-muted uppercase tracking-wider">Pricing</h4>
|
|
538
|
+
<div className="space-y-3">
|
|
539
|
+
<DetailRow label="Model" value={api.pricingModel} />
|
|
540
|
+
{api.pricePerCall && (
|
|
541
|
+
<DetailRow
|
|
542
|
+
label="Price per call"
|
|
543
|
+
value={`$${(api.pricePerCall / 100).toFixed(4)}`}
|
|
544
|
+
/>
|
|
545
|
+
)}
|
|
546
|
+
<DetailRow
|
|
547
|
+
label="Regions"
|
|
548
|
+
value={api.regions?.join(", ") || "Global"}
|
|
549
|
+
/>
|
|
550
|
+
<DetailRow
|
|
551
|
+
label="Tags"
|
|
552
|
+
value={
|
|
553
|
+
<div className="flex flex-wrap gap-1">
|
|
554
|
+
{api.tags?.map((tag) => (
|
|
555
|
+
<span key={tag} className="px-2 py-0.5 bg-surface rounded text-xs">
|
|
556
|
+
{tag}
|
|
557
|
+
</span>
|
|
558
|
+
))}
|
|
559
|
+
</div>
|
|
560
|
+
}
|
|
561
|
+
/>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
|
|
566
|
+
{/* Credentials */}
|
|
567
|
+
<div className="p-6">
|
|
568
|
+
<div className="flex items-center justify-between mb-4">
|
|
569
|
+
<h4 className="font-semibold text-sm text-text-muted uppercase tracking-wider">Credentials</h4>
|
|
570
|
+
<button className="text-sm text-accent hover:underline flex items-center gap-1">
|
|
571
|
+
<RefreshCw className="w-4 h-4" /> Regenerate
|
|
572
|
+
</button>
|
|
573
|
+
</div>
|
|
574
|
+
<div className="space-y-3">
|
|
575
|
+
<div className="flex items-center justify-between p-3 bg-surface rounded-lg">
|
|
576
|
+
<div>
|
|
577
|
+
<p className="text-sm text-text-muted mb-1">API Key</p>
|
|
578
|
+
<p className="font-mono text-sm">{credentials.apiKey}</p>
|
|
579
|
+
</div>
|
|
580
|
+
<button
|
|
581
|
+
onClick={() => copyToClipboard(credentials.apiKey, "api")}
|
|
582
|
+
className="p-2 hover:bg-surface-elevated rounded-lg transition"
|
|
583
|
+
>
|
|
584
|
+
{copied === "api" ? (
|
|
585
|
+
<Check className="w-4 h-4 text-green-500" />
|
|
586
|
+
) : (
|
|
587
|
+
<Copy className="w-4 h-4 text-text-muted" />
|
|
588
|
+
)}
|
|
589
|
+
</button>
|
|
590
|
+
</div>
|
|
591
|
+
{credentials.secretKey && (
|
|
592
|
+
<div className="flex items-center justify-between p-3 bg-surface rounded-lg">
|
|
593
|
+
<div>
|
|
594
|
+
<p className="text-sm text-text-muted mb-1">Secret Key</p>
|
|
595
|
+
<p className="font-mono text-sm">
|
|
596
|
+
{showSecret ? credentials.secretKey : "sk_live_••••••••••••••••••••••••"}
|
|
597
|
+
</p>
|
|
598
|
+
</div>
|
|
599
|
+
<div className="flex items-center gap-1">
|
|
600
|
+
<button
|
|
601
|
+
onClick={() => setShowSecret(!showSecret)}
|
|
602
|
+
className="p-2 hover:bg-surface-elevated rounded-lg transition"
|
|
603
|
+
>
|
|
604
|
+
{showSecret ? (
|
|
605
|
+
<EyeOff className="w-4 h-4 text-text-muted" />
|
|
606
|
+
) : (
|
|
607
|
+
<Eye className="w-4 h-4 text-text-muted" />
|
|
608
|
+
)}
|
|
609
|
+
</button>
|
|
610
|
+
<button
|
|
611
|
+
onClick={() => copyToClipboard(credentials.secretKey!, "secret")}
|
|
612
|
+
className="p-2 hover:bg-surface-elevated rounded-lg transition"
|
|
613
|
+
>
|
|
614
|
+
{copied === "secret" ? (
|
|
615
|
+
<Check className="w-4 h-4 text-green-500" />
|
|
616
|
+
) : (
|
|
617
|
+
<Copy className="w-4 h-4 text-text-muted" />
|
|
618
|
+
)}
|
|
619
|
+
</button>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
)}
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function DetailRow({
|
|
630
|
+
label,
|
|
631
|
+
value,
|
|
632
|
+
mono,
|
|
633
|
+
}: {
|
|
634
|
+
label: string;
|
|
635
|
+
value: React.ReactNode;
|
|
636
|
+
mono?: boolean;
|
|
637
|
+
}) {
|
|
638
|
+
return (
|
|
639
|
+
<div className="flex items-center justify-between">
|
|
640
|
+
<span className="text-sm text-text-muted">{label}</span>
|
|
641
|
+
<span className={`text-sm ${mono ? "font-mono" : ""}`}>{value}</span>
|
|
642
|
+
</div>
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ============================================
|
|
647
|
+
// EARNINGS TAB
|
|
648
|
+
// ============================================
|
|
649
|
+
|
|
650
|
+
function EarningsTab({ earnings }: { earnings: ReturnType<typeof getMockEarnings> }) {
|
|
651
|
+
return (
|
|
652
|
+
<div className="space-y-8">
|
|
653
|
+
<h2 className="text-2xl font-bold">Earnings</h2>
|
|
654
|
+
|
|
655
|
+
{/* Earnings Stats */}
|
|
656
|
+
<div className="grid md:grid-cols-3 gap-4">
|
|
657
|
+
<div className="bg-accent/10 border border-accent/30 rounded-2xl p-6">
|
|
658
|
+
<div className="flex items-center gap-2 mb-3">
|
|
659
|
+
<Clock className="w-5 h-5 text-accent" />
|
|
660
|
+
<span className="text-sm text-text-muted">Pending Payout</span>
|
|
661
|
+
</div>
|
|
662
|
+
<p className="text-4xl font-bold text-accent">${earnings.pendingAmount.toFixed(2)}</p>
|
|
663
|
+
<p className="text-sm text-text-muted mt-2">Available for payout</p>
|
|
664
|
+
</div>
|
|
665
|
+
<div className="bg-surface-elevated border border-border rounded-2xl p-6">
|
|
666
|
+
<div className="flex items-center gap-2 mb-3">
|
|
667
|
+
<DollarSign className="w-5 h-5 text-text-muted" />
|
|
668
|
+
<span className="text-sm text-text-muted">Total Earned</span>
|
|
669
|
+
</div>
|
|
670
|
+
<p className="text-4xl font-bold">${earnings.totalEarned.toFixed(2)}</p>
|
|
671
|
+
<p className="text-sm text-text-muted mt-2">All time</p>
|
|
672
|
+
</div>
|
|
673
|
+
<div className="bg-surface-elevated border border-border rounded-2xl p-6">
|
|
674
|
+
<div className="flex items-center gap-2 mb-3">
|
|
675
|
+
<Check className="w-5 h-5 text-green-500" />
|
|
676
|
+
<span className="text-sm text-text-muted">Total Paid Out</span>
|
|
677
|
+
</div>
|
|
678
|
+
<p className="text-4xl font-bold">${earnings.totalPaidOut.toFixed(2)}</p>
|
|
679
|
+
<p className="text-sm text-text-muted mt-2">Successfully transferred</p>
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
|
|
683
|
+
{/* Stripe Connect */}
|
|
684
|
+
<div className="bg-surface-elevated border border-border rounded-2xl p-6">
|
|
685
|
+
<div className="flex items-center justify-between">
|
|
686
|
+
<div>
|
|
687
|
+
<h3 className="font-semibold text-lg mb-1">Payout Settings</h3>
|
|
688
|
+
<p className="text-sm text-text-muted">
|
|
689
|
+
Connect your Stripe account to receive payouts
|
|
690
|
+
</p>
|
|
691
|
+
</div>
|
|
692
|
+
{earnings.stripeOnboardingComplete ? (
|
|
693
|
+
<div className="flex items-center gap-2 text-green-500">
|
|
694
|
+
<Check className="w-5 h-5" />
|
|
695
|
+
<span className="font-medium">Connected</span>
|
|
696
|
+
</div>
|
|
697
|
+
) : (
|
|
698
|
+
<button className="btn-primary !py-2 !px-4">
|
|
699
|
+
<CreditCard className="w-4 h-4" />
|
|
700
|
+
Connect Stripe
|
|
701
|
+
</button>
|
|
702
|
+
)}
|
|
703
|
+
</div>
|
|
704
|
+
{earnings.stripeOnboardingComplete && (
|
|
705
|
+
<div className="mt-4 pt-4 border-t border-border flex items-center justify-between">
|
|
706
|
+
<div className="flex items-center gap-3">
|
|
707
|
+
<div className="w-10 h-10 rounded-lg bg-[#635BFF]/20 flex items-center justify-center">
|
|
708
|
+
<svg viewBox="0 0 24 24" className="w-6 h-6 text-[#635BFF]" fill="currentColor">
|
|
709
|
+
<path d="M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.594-7.305h.003z" />
|
|
710
|
+
</svg>
|
|
711
|
+
</div>
|
|
712
|
+
<div>
|
|
713
|
+
<p className="font-medium">Stripe Connect</p>
|
|
714
|
+
<p className="text-sm text-text-muted">Account verified and ready for payouts</p>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
<button className="text-sm text-accent hover:underline flex items-center gap-1">
|
|
718
|
+
Manage <ChevronRight className="w-4 h-4" />
|
|
719
|
+
</button>
|
|
720
|
+
</div>
|
|
721
|
+
)}
|
|
722
|
+
</div>
|
|
723
|
+
|
|
724
|
+
{/* Payout History */}
|
|
725
|
+
<div className="bg-surface-elevated border border-border rounded-2xl overflow-hidden">
|
|
726
|
+
<div className="p-6 border-b border-border">
|
|
727
|
+
<h3 className="font-semibold text-lg">Payout History</h3>
|
|
728
|
+
</div>
|
|
729
|
+
{earnings.payouts.length > 0 ? (
|
|
730
|
+
<div className="divide-y divide-border">
|
|
731
|
+
{earnings.payouts.map((payout) => (
|
|
732
|
+
<div key={payout.id} className="p-4 flex items-center justify-between">
|
|
733
|
+
<div className="flex items-center gap-4">
|
|
734
|
+
<div
|
|
735
|
+
className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
|
736
|
+
payout.status === "completed"
|
|
737
|
+
? "bg-green-500/20"
|
|
738
|
+
: payout.status === "processing"
|
|
739
|
+
? "bg-yellow-500/20"
|
|
740
|
+
: payout.status === "pending"
|
|
741
|
+
? "bg-blue-500/20"
|
|
742
|
+
: "bg-red-500/20"
|
|
743
|
+
}`}
|
|
744
|
+
>
|
|
745
|
+
{payout.status === "completed" ? (
|
|
746
|
+
<Check className="w-5 h-5 text-green-500" />
|
|
747
|
+
) : payout.status === "processing" ? (
|
|
748
|
+
<RefreshCw className="w-5 h-5 text-yellow-600 animate-spin" />
|
|
749
|
+
) : payout.status === "pending" ? (
|
|
750
|
+
<Clock className="w-5 h-5 text-blue-500" />
|
|
751
|
+
) : (
|
|
752
|
+
<AlertCircle className="w-5 h-5 text-red-500" />
|
|
753
|
+
)}
|
|
754
|
+
</div>
|
|
755
|
+
<div>
|
|
756
|
+
<p className="font-medium">${payout.amount.toFixed(2)}</p>
|
|
757
|
+
<p className="text-sm text-text-muted">
|
|
758
|
+
{new Date(payout.periodStart).toLocaleDateString()} -{" "}
|
|
759
|
+
{new Date(payout.periodEnd).toLocaleDateString()}
|
|
760
|
+
</p>
|
|
761
|
+
</div>
|
|
762
|
+
</div>
|
|
763
|
+
<div className="text-right">
|
|
764
|
+
<span
|
|
765
|
+
className={`px-2 py-1 rounded-full text-xs font-medium capitalize ${
|
|
766
|
+
payout.status === "completed"
|
|
767
|
+
? "bg-green-500/20 text-green-500"
|
|
768
|
+
: payout.status === "processing"
|
|
769
|
+
? "bg-yellow-500/20 text-yellow-600"
|
|
770
|
+
: payout.status === "pending"
|
|
771
|
+
? "bg-blue-500/20 text-blue-500"
|
|
772
|
+
: "bg-red-500/20 text-red-500"
|
|
773
|
+
}`}
|
|
774
|
+
>
|
|
775
|
+
{payout.status}
|
|
776
|
+
</span>
|
|
777
|
+
{payout.completedAt && (
|
|
778
|
+
<p className="text-xs text-text-muted mt-1">
|
|
779
|
+
{new Date(payout.completedAt).toLocaleDateString()}
|
|
780
|
+
</p>
|
|
781
|
+
)}
|
|
782
|
+
</div>
|
|
783
|
+
</div>
|
|
784
|
+
))}
|
|
785
|
+
</div>
|
|
786
|
+
) : (
|
|
787
|
+
<div className="p-12 text-center text-text-muted">
|
|
788
|
+
<p>No payouts yet</p>
|
|
789
|
+
</div>
|
|
790
|
+
)}
|
|
791
|
+
</div>
|
|
792
|
+
</div>
|
|
793
|
+
);
|
|
794
|
+
}
|