@nordsym/apiclaw 1.3.3 → 1.3.5

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.
Files changed (42) hide show
  1. package/convex/_generated/api.d.ts +6 -0
  2. package/convex/billing.ts +341 -0
  3. package/convex/email.ts +276 -0
  4. package/convex/http.ts +154 -0
  5. package/convex/schema.ts +43 -0
  6. package/convex/workspaces.ts +663 -0
  7. package/dist/cli.d.ts +7 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +272 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/index.js +396 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/session.d.ts +29 -0
  14. package/dist/session.d.ts.map +1 -0
  15. package/dist/session.js +87 -0
  16. package/dist/session.js.map +1 -0
  17. package/docs/PRD-agent-first-billing.md +525 -0
  18. package/docs/PRD-workspace-fixes.md +178 -0
  19. package/landing/package-lock.json +21 -3
  20. package/landing/package.json +2 -1
  21. package/landing/src/app/api/stripe/webhook/route.ts +178 -0
  22. package/landing/src/app/api/workspace-auth/magic-link/route.ts +84 -0
  23. package/landing/src/app/api/workspace-auth/session/route.ts +73 -0
  24. package/landing/src/app/api/workspace-auth/verify/route.ts +57 -0
  25. package/landing/src/app/auth/verify/page.tsx +292 -0
  26. package/landing/src/app/dashboard/layout.tsx +22 -0
  27. package/landing/src/app/dashboard/page.tsx +22 -0
  28. package/landing/src/app/dashboard/verify/page.tsx +108 -0
  29. package/landing/src/app/login/page.tsx +204 -0
  30. package/landing/src/app/page.tsx +23 -7
  31. package/landing/src/app/providers/dashboard/layout.tsx +5 -4
  32. package/landing/src/app/providers/dashboard/page.tsx +11 -641
  33. package/landing/src/app/upgrade/page.tsx +288 -0
  34. package/landing/src/app/workspace/layout.tsx +30 -0
  35. package/landing/src/app/workspace/page.tsx +1637 -0
  36. package/landing/src/lib/stats.json +14 -15
  37. package/landing/src/middleware.ts +50 -0
  38. package/landing/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +1 -1
  40. package/src/cli.ts +320 -0
  41. package/src/index.ts +444 -4
  42. package/src/session.ts +103 -0
@@ -0,0 +1,292 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useSearchParams } from "next/navigation";
5
+ import { Suspense } from "react";
6
+
7
+ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://brilliant-puffin-712.eu-west-1.convex.cloud";
8
+
9
+ interface VerifyResult {
10
+ success: boolean;
11
+ error?: string;
12
+ workspace?: {
13
+ id: string;
14
+ email: string;
15
+ status: string;
16
+ tier: string;
17
+ usageCount: number;
18
+ usageLimit: number;
19
+ };
20
+ sessionToken?: string;
21
+ }
22
+
23
+ function VerifyContent() {
24
+ const searchParams = useSearchParams();
25
+ const token = searchParams.get("token");
26
+
27
+ const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
28
+ const [error, setError] = useState<string>("");
29
+ const [workspace, setWorkspace] = useState<VerifyResult["workspace"] | null>(null);
30
+ const [sessionToken, setSessionToken] = useState<string>("");
31
+
32
+ // Password form state
33
+ const [showPasswordForm, setShowPasswordForm] = useState(false);
34
+ const [password, setPassword] = useState("");
35
+ const [confirmPassword, setConfirmPassword] = useState("");
36
+ const [passwordError, setPasswordError] = useState("");
37
+ const [passwordSaved, setPasswordSaved] = useState(false);
38
+
39
+ useEffect(() => {
40
+ if (!token) {
41
+ setStatus("error");
42
+ setError("No verification token provided");
43
+ return;
44
+ }
45
+
46
+ verifyToken(token);
47
+ }, [token]);
48
+
49
+ async function verifyToken(token: string) {
50
+ try {
51
+ const response = await fetch(`${CONVEX_URL}/api/mutation`, {
52
+ method: "POST",
53
+ headers: { "Content-Type": "application/json" },
54
+ body: JSON.stringify({
55
+ path: "workspaces:verifyMagicLink",
56
+ args: { token },
57
+ }),
58
+ });
59
+
60
+ if (!response.ok) {
61
+ throw new Error("Verification failed");
62
+ }
63
+
64
+ const result = await response.json();
65
+ const data: VerifyResult = result.value || result;
66
+
67
+ if (data.success) {
68
+ setStatus("success");
69
+ setWorkspace(data.workspace || null);
70
+ setSessionToken(data.sessionToken || "");
71
+ } else {
72
+ setStatus("error");
73
+ switch (data.error) {
74
+ case "invalid_token":
75
+ setError("This verification link is invalid.");
76
+ break;
77
+ case "already_used":
78
+ setError("This verification link has already been used.");
79
+ break;
80
+ case "expired":
81
+ setError("This verification link has expired. Please request a new one.");
82
+ break;
83
+ default:
84
+ setError("Verification failed. Please try again.");
85
+ }
86
+ }
87
+ } catch (err) {
88
+ setStatus("error");
89
+ setError("Something went wrong. Please try again.");
90
+ console.error("Verification error:", err);
91
+ }
92
+ }
93
+
94
+ async function handleSetPassword(e: React.FormEvent) {
95
+ e.preventDefault();
96
+ setPasswordError("");
97
+
98
+ if (password.length < 8) {
99
+ setPasswordError("Password must be at least 8 characters");
100
+ return;
101
+ }
102
+
103
+ if (password !== confirmPassword) {
104
+ setPasswordError("Passwords don't match");
105
+ return;
106
+ }
107
+
108
+ try {
109
+ const response = await fetch(`${CONVEX_URL}/api/mutation`, {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify({
113
+ path: "workspaces:setPassword",
114
+ args: { sessionToken, password },
115
+ }),
116
+ });
117
+
118
+ const result = await response.json();
119
+ const data = result.value || result;
120
+
121
+ if (data.success) {
122
+ setPasswordSaved(true);
123
+ setShowPasswordForm(false);
124
+ } else {
125
+ setPasswordError("Failed to set password. Please try again.");
126
+ }
127
+ } catch (err) {
128
+ setPasswordError("Something went wrong. Please try again.");
129
+ }
130
+ }
131
+
132
+ return (
133
+ <div className="min-h-screen bg-[var(--background)] flex items-center justify-center p-4">
134
+ <div className="w-full max-w-md">
135
+ {/* Logo */}
136
+ <div className="text-center mb-8">
137
+ <span className="text-6xl">🦞</span>
138
+ <h1 className="text-2xl font-bold mt-4">APIClaw</h1>
139
+ </div>
140
+
141
+ {/* Loading state */}
142
+ {status === "loading" && (
143
+ <div className="bg-[var(--surface)] rounded-xl p-8 text-center border border-[var(--border)]">
144
+ <div className="animate-spin w-8 h-8 border-4 border-[var(--accent)] border-t-transparent rounded-full mx-auto mb-4" />
145
+ <p className="text-[var(--text-secondary)]">Verifying your email...</p>
146
+ </div>
147
+ )}
148
+
149
+ {/* Success state */}
150
+ {status === "success" && (
151
+ <div className="bg-[var(--surface)] rounded-xl p-8 border border-[var(--border)]">
152
+ <div className="text-center mb-6">
153
+ <div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
154
+ <svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
155
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
156
+ </svg>
157
+ </div>
158
+ <h2 className="text-xl font-bold mb-2">Email Verified!</h2>
159
+ <p className="text-[var(--text-secondary)]">
160
+ Your workspace is now active. Your AI agent can start using APIClaw.
161
+ </p>
162
+ </div>
163
+
164
+ {workspace && (
165
+ <div className="bg-[var(--background)] rounded-lg p-4 mb-6">
166
+ <div className="space-y-2 text-sm">
167
+ <div className="flex justify-between">
168
+ <span className="text-[var(--text-muted)]">Email</span>
169
+ <span className="font-medium">{workspace.email}</span>
170
+ </div>
171
+ <div className="flex justify-between">
172
+ <span className="text-[var(--text-muted)]">Plan</span>
173
+ <span className="font-medium capitalize">{workspace.tier}</span>
174
+ </div>
175
+ <div className="flex justify-between">
176
+ <span className="text-[var(--text-muted)]">API Calls</span>
177
+ <span className="font-medium">{workspace.usageCount} / {workspace.usageLimit}</span>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ )}
182
+
183
+ {/* Password form */}
184
+ {!passwordSaved && !showPasswordForm && (
185
+ <button
186
+ onClick={() => setShowPasswordForm(true)}
187
+ className="w-full py-3 px-4 rounded-lg border border-[var(--border)] text-[var(--text-secondary)] hover:bg-[var(--background)] transition-colors text-sm mb-4"
188
+ >
189
+ Set a password (optional)
190
+ </button>
191
+ )}
192
+
193
+ {showPasswordForm && (
194
+ <form onSubmit={handleSetPassword} className="mb-4">
195
+ <div className="space-y-3">
196
+ <input
197
+ type="password"
198
+ placeholder="Password (min. 8 characters)"
199
+ value={password}
200
+ onChange={(e) => setPassword(e.target.value)}
201
+ className="w-full px-4 py-3 rounded-lg bg-[var(--background)] border border-[var(--border)] focus:border-[var(--accent)] focus:ring-1 focus:ring-[var(--accent)] outline-none transition-colors"
202
+ />
203
+ <input
204
+ type="password"
205
+ placeholder="Confirm password"
206
+ value={confirmPassword}
207
+ onChange={(e) => setConfirmPassword(e.target.value)}
208
+ className="w-full px-4 py-3 rounded-lg bg-[var(--background)] border border-[var(--border)] focus:border-[var(--accent)] focus:ring-1 focus:ring-[var(--accent)] outline-none transition-colors"
209
+ />
210
+ </div>
211
+ {passwordError && (
212
+ <p className="text-red-500 text-sm mt-2">{passwordError}</p>
213
+ )}
214
+ <div className="flex gap-2 mt-3">
215
+ <button
216
+ type="button"
217
+ onClick={() => setShowPasswordForm(false)}
218
+ className="flex-1 py-2 px-4 rounded-lg border border-[var(--border)] text-[var(--text-secondary)] hover:bg-[var(--background)] transition-colors text-sm"
219
+ >
220
+ Cancel
221
+ </button>
222
+ <button
223
+ type="submit"
224
+ className="flex-1 py-2 px-4 rounded-lg bg-[var(--accent)] text-white font-medium hover:opacity-90 transition-opacity text-sm"
225
+ >
226
+ Save Password
227
+ </button>
228
+ </div>
229
+ </form>
230
+ )}
231
+
232
+ {passwordSaved && (
233
+ <div className="bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 rounded-lg p-3 text-sm text-center mb-4">
234
+ ✓ Password saved
235
+ </div>
236
+ )}
237
+
238
+ <div className="text-center pt-4 border-t border-[var(--border)]">
239
+ <p className="text-[var(--text-muted)] text-sm">
240
+ ✓ You can close this tab now
241
+ </p>
242
+ </div>
243
+ </div>
244
+ )}
245
+
246
+ {/* Error state */}
247
+ {status === "error" && (
248
+ <div className="bg-[var(--surface)] rounded-xl p-8 border border-[var(--border)]">
249
+ <div className="text-center">
250
+ <div className="w-16 h-16 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
251
+ <svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
252
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
253
+ </svg>
254
+ </div>
255
+ <h2 className="text-xl font-bold mb-2">Verification Failed</h2>
256
+ <p className="text-[var(--text-secondary)] mb-6">{error}</p>
257
+ <a
258
+ href="/"
259
+ className="inline-block py-3 px-6 rounded-lg bg-[var(--accent)] text-white font-medium hover:opacity-90 transition-opacity"
260
+ >
261
+ Go to Homepage
262
+ </a>
263
+ </div>
264
+ </div>
265
+ )}
266
+
267
+ {/* Footer */}
268
+ <p className="text-center text-[var(--text-muted)] text-xs mt-6">
269
+ Having trouble? Contact{" "}
270
+ <a href="mailto:support@apiclaw.com" className="text-[var(--accent)] hover:underline">
271
+ support@apiclaw.com
272
+ </a>
273
+ </p>
274
+ </div>
275
+ </div>
276
+ );
277
+ }
278
+
279
+ export default function VerifyPage() {
280
+ return (
281
+ <Suspense fallback={
282
+ <div className="min-h-screen bg-[var(--background)] flex items-center justify-center">
283
+ <div className="text-center">
284
+ <span className="text-6xl">🦞</span>
285
+ <p className="mt-4 text-[var(--text-secondary)]">Loading...</p>
286
+ </div>
287
+ </div>
288
+ }>
289
+ <VerifyContent />
290
+ </Suspense>
291
+ );
292
+ }
@@ -0,0 +1,22 @@
1
+ import { Suspense } from "react";
2
+
3
+ export const metadata = {
4
+ title: "Dashboard | APIClaw",
5
+ description: "Manage your AI agents and view API usage analytics",
6
+ };
7
+
8
+ export default function DashboardLayout({
9
+ children,
10
+ }: {
11
+ children: React.ReactNode;
12
+ }) {
13
+ return (
14
+ <Suspense fallback={
15
+ <div className="min-h-screen flex items-center justify-center bg-[var(--background)]">
16
+ <div className="w-8 h-8 border-4 border-accent border-t-transparent rounded-full animate-spin" />
17
+ </div>
18
+ }>
19
+ {children}
20
+ </Suspense>
21
+ );
22
+ }
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Loader2 } from "lucide-react";
6
+
7
+ export default function DashboardRedirect() {
8
+ const router = useRouter();
9
+
10
+ useEffect(() => {
11
+ router.replace("/workspace");
12
+ }, [router]);
13
+
14
+ return (
15
+ <div className="min-h-screen flex items-center justify-center bg-[var(--background)]">
16
+ <div className="text-center">
17
+ <Loader2 className="w-12 h-12 text-[#ef4444] animate-spin mx-auto mb-4" />
18
+ <p className="text-[var(--text-muted)]">Redirecting to workspace...</p>
19
+ </div>
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,108 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, Suspense } from "react";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
+ import { Loader2, AlertCircle, Check } from "lucide-react";
6
+ import Link from "next/link";
7
+
8
+ function VerifyContent() {
9
+ const router = useRouter();
10
+ const searchParams = useSearchParams();
11
+ const token = searchParams.get("token");
12
+
13
+ const [status, setStatus] = useState<"verifying" | "success" | "error">("verifying");
14
+ const [error, setError] = useState<string | null>(null);
15
+
16
+ useEffect(() => {
17
+ if (!token) {
18
+ setStatus("error");
19
+ setError("No verification token provided");
20
+ return;
21
+ }
22
+
23
+ const verify = async () => {
24
+ try {
25
+ const response = await fetch("/api/workspace-auth/verify", {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({ token }),
29
+ });
30
+
31
+ const data = await response.json();
32
+
33
+ if (!response.ok || !data.success) {
34
+ throw new Error(data.error || "Verification failed");
35
+ }
36
+
37
+ // Store session token in localStorage as backup
38
+ if (data.sessionToken) {
39
+ localStorage.setItem("apiclaw_workspace_session", data.sessionToken);
40
+ }
41
+
42
+ setStatus("success");
43
+
44
+ // Redirect to dashboard after short delay
45
+ setTimeout(() => {
46
+ router.push("/workspace");
47
+ }, 1500);
48
+ } catch (err) {
49
+ setStatus("error");
50
+ setError(err instanceof Error ? err.message : "Verification failed");
51
+ }
52
+ };
53
+
54
+ verify();
55
+ }, [token, router]);
56
+
57
+ return (
58
+ <main className="min-h-screen flex items-center justify-center px-6 bg-[var(--background)]">
59
+ <div className="max-w-md w-full text-center">
60
+ {status === "verifying" && (
61
+ <>
62
+ <Loader2 className="w-16 h-16 text-accent animate-spin mx-auto mb-6" />
63
+ <h1 className="text-2xl font-bold mb-2">Verifying...</h1>
64
+ <p className="text-[var(--text-muted)]">Please wait while we sign you in.</p>
65
+ </>
66
+ )}
67
+
68
+ {status === "success" && (
69
+ <>
70
+ <div className="w-20 h-20 rounded-full bg-green-500/20 flex items-center justify-center mx-auto mb-6">
71
+ <Check className="w-10 h-10 text-green-500" />
72
+ </div>
73
+ <h1 className="text-2xl font-bold mb-2">Success!</h1>
74
+ <p className="text-[var(--text-muted)]">Redirecting to your dashboard...</p>
75
+ </>
76
+ )}
77
+
78
+ {status === "error" && (
79
+ <>
80
+ <div className="w-20 h-20 rounded-full bg-red-500/20 flex items-center justify-center mx-auto mb-6">
81
+ <AlertCircle className="w-10 h-10 text-red-500" />
82
+ </div>
83
+ <h1 className="text-2xl font-bold mb-2">Verification Failed</h1>
84
+ <p className="text-red-500 mb-6">{error}</p>
85
+ <Link href="/login" className="btn-primary">
86
+ Try Again
87
+ </Link>
88
+ </>
89
+ )}
90
+ </div>
91
+ </main>
92
+ );
93
+ }
94
+
95
+ export default function VerifyPage() {
96
+ return (
97
+ <Suspense fallback={
98
+ <main className="min-h-screen flex items-center justify-center px-6 bg-[var(--background)]">
99
+ <div className="text-center">
100
+ <Loader2 className="w-16 h-16 text-accent animate-spin mx-auto mb-6" />
101
+ <p className="text-[var(--text-muted)]">Loading...</p>
102
+ </div>
103
+ </main>
104
+ }>
105
+ <VerifyContent />
106
+ </Suspense>
107
+ );
108
+ }
@@ -0,0 +1,204 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { Mail, Loader2, Check, ArrowRight, Sun, Moon, Zap } from "lucide-react";
5
+ import Link from "next/link";
6
+ import { useRouter } from "next/navigation";
7
+
8
+ export default function LoginPage() {
9
+ const router = useRouter();
10
+ const [isDark, setIsDark] = useState(true);
11
+ const [email, setEmail] = useState("");
12
+ const [isLoading, setIsLoading] = useState(false);
13
+ const [isSent, setIsSent] = useState(false);
14
+ const [error, setError] = useState<string | null>(null);
15
+
16
+ useEffect(() => {
17
+ // Check if already logged in
18
+ const checkSession = async () => {
19
+ try {
20
+ const res = await fetch("/api/workspace-auth/session");
21
+ const data = await res.json();
22
+ if (data.session) {
23
+ router.push("/workspace");
24
+ return;
25
+ }
26
+ } catch {
27
+ // Not logged in, continue
28
+ }
29
+ };
30
+ checkSession();
31
+
32
+ const saved = localStorage.getItem("theme");
33
+ const prefersDark = saved ? saved === "dark" : true;
34
+ setIsDark(prefersDark);
35
+ document.documentElement.classList.toggle("dark", prefersDark);
36
+ }, [router]);
37
+
38
+ const toggleTheme = () => {
39
+ const newTheme = !isDark;
40
+ setIsDark(newTheme);
41
+ document.documentElement.classList.toggle("dark", newTheme);
42
+ localStorage.setItem("theme", newTheme ? "dark" : "light");
43
+ };
44
+
45
+ const handleSubmit = async (e: React.FormEvent) => {
46
+ e.preventDefault();
47
+ setIsLoading(true);
48
+ setError(null);
49
+
50
+ try {
51
+ const response = await fetch("/api/workspace-auth/magic-link", {
52
+ method: "POST",
53
+ headers: { "Content-Type": "application/json" },
54
+ body: JSON.stringify({ email }),
55
+ });
56
+
57
+ if (!response.ok) {
58
+ const data = await response.json();
59
+ throw new Error(data.error || "Failed to send magic link");
60
+ }
61
+
62
+ setIsSent(true);
63
+ } catch (err) {
64
+ setError(err instanceof Error ? err.message : "Something went wrong");
65
+ } finally {
66
+ setIsLoading(false);
67
+ }
68
+ };
69
+
70
+ if (isSent) {
71
+ return (
72
+ <main className="min-h-screen flex items-center justify-center px-6 bg-[var(--background)]">
73
+ <div className="max-w-md w-full text-center">
74
+ <div className="w-20 h-20 rounded-full bg-accent/20 flex items-center justify-center mx-auto mb-8">
75
+ <Check className="w-10 h-10 text-accent" />
76
+ </div>
77
+ <h1 className="text-3xl font-bold mb-4">Check Your Email</h1>
78
+ <p className="text-[var(--text-secondary)] mb-2">
79
+ We&apos;ve sent a magic link to:
80
+ </p>
81
+ <p className="font-semibold text-lg mb-8">{email}</p>
82
+ <p className="text-sm text-[var(--text-muted)]">
83
+ Click the link in the email to sign in. The link expires in 15 minutes.
84
+ </p>
85
+ <button
86
+ onClick={() => setIsSent(false)}
87
+ className="mt-8 text-accent hover:underline"
88
+ >
89
+ Use a different email
90
+ </button>
91
+ </div>
92
+ </main>
93
+ );
94
+ }
95
+
96
+ return (
97
+ <main className="min-h-screen bg-[var(--background)]">
98
+ {/* Header */}
99
+ <header className="fixed top-0 w-full z-50 bg-[var(--background)]/90 backdrop-blur-xl border-b border-[var(--border-subtle)]">
100
+ <div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
101
+ <Link href="/" className="flex items-center gap-3">
102
+ <div className="w-9 h-9 rounded-xl bg-accent/20 flex items-center justify-center text-xl">
103
+ 🦞
104
+ </div>
105
+ <span className="font-bold text-lg tracking-tight">APIClaw</span>
106
+ </Link>
107
+ <button
108
+ onClick={toggleTheme}
109
+ className="p-2 rounded-lg hover:bg-[var(--surface)] transition"
110
+ aria-label="Toggle theme"
111
+ >
112
+ {isDark ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
113
+ </button>
114
+ </div>
115
+ </header>
116
+
117
+ <div className="pt-32 pb-24 px-6">
118
+ <div className="max-w-md mx-auto">
119
+ <div className="text-center mb-8">
120
+ <div className="w-16 h-16 rounded-2xl bg-accent/20 flex items-center justify-center mx-auto mb-6">
121
+ <Zap className="w-8 h-8 text-accent" />
122
+ </div>
123
+ <h1 className="text-3xl font-bold mb-2">Agent Workspace</h1>
124
+ <p className="text-[var(--text-secondary)]">
125
+ Sign in to manage your AI agents and view usage
126
+ </p>
127
+ </div>
128
+
129
+ <div className="rounded-2xl bg-[var(--surface-elevated)] border border-[var(--border)] p-8">
130
+ <form onSubmit={handleSubmit} className="space-y-6">
131
+ <div>
132
+ <label className="block text-sm font-medium mb-2">Email</label>
133
+ <div className="relative">
134
+ <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)]" />
135
+ <input
136
+ type="email"
137
+ value={email}
138
+ onChange={(e) => {
139
+ setEmail(e.target.value);
140
+ setError(null);
141
+ }}
142
+ placeholder="you@company.com"
143
+ required
144
+ className="w-full pl-12 pr-4 py-3 rounded-xl bg-[var(--surface)] border border-[var(--border)] focus:border-accent focus:outline-none transition"
145
+ />
146
+ </div>
147
+ </div>
148
+
149
+ {error && (
150
+ <div className="p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-500 text-sm">
151
+ {error}
152
+ </div>
153
+ )}
154
+
155
+ <button
156
+ type="submit"
157
+ disabled={isLoading || !email}
158
+ className="btn-primary w-full justify-center disabled:opacity-50"
159
+ >
160
+ {isLoading ? (
161
+ <>
162
+ <Loader2 className="w-5 h-5 animate-spin" />
163
+ Sending...
164
+ </>
165
+ ) : (
166
+ <>
167
+ Send Magic Link
168
+ <ArrowRight className="w-5 h-5" />
169
+ </>
170
+ )}
171
+ </button>
172
+ </form>
173
+
174
+ <div className="mt-6 pt-6 border-t border-[var(--border)]">
175
+ <div className="space-y-3 text-sm text-[var(--text-muted)]">
176
+ <div className="flex items-center gap-2">
177
+ <Check className="w-4 h-4 text-green-500" />
178
+ <span>1,000 free API calls per month</span>
179
+ </div>
180
+ <div className="flex items-center gap-2">
181
+ <Check className="w-4 h-4 text-green-500" />
182
+ <span>Connect unlimited AI agents</span>
183
+ </div>
184
+ <div className="flex items-center gap-2">
185
+ <Check className="w-4 h-4 text-green-500" />
186
+ <span>Usage analytics & monitoring</span>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+
192
+ <div className="mt-6 text-center">
193
+ <p className="text-sm text-[var(--text-muted)]">
194
+ Are you an API provider?{" "}
195
+ <Link href="/providers/dashboard/login" className="text-accent hover:underline">
196
+ Provider Dashboard
197
+ </Link>
198
+ </p>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </main>
203
+ );
204
+ }