@nordsym/apiclaw 1.0.0 → 1.1.0
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 +197 -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 +6 -6
- package/convex/credits.ts +211 -0
- package/convex/http.ts +490 -0
- package/convex/providers.ts +516 -0
- package/convex/purchases.ts +183 -0
- package/convex/schema.ts +180 -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 +106 -30
- 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/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 +1190 -40
- package/landing/package.json +5 -2
- 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-image.png +0 -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 +74 -0
- package/landing/src/app/globals.css +439 -100
- package/landing/src/app/layout.tsx +37 -9
- package/landing/src/app/page.tsx +640 -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 +113 -31
- 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/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,670 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ArrowRight, ArrowLeft, Check, Loader2, Sun, Moon, Github, AlertCircle,
|
|
5
|
+
FileJson, Globe, DollarSign, Tag, Sparkles, ExternalLink
|
|
6
|
+
} from "lucide-react";
|
|
7
|
+
import { useState, useEffect } from "react";
|
|
8
|
+
import Link from "next/link";
|
|
9
|
+
|
|
10
|
+
const categories = [
|
|
11
|
+
"SMS & Messaging",
|
|
12
|
+
"Email",
|
|
13
|
+
"Search",
|
|
14
|
+
"AI & LLM",
|
|
15
|
+
"Voice & TTS",
|
|
16
|
+
"Crypto & Blockchain",
|
|
17
|
+
"Market Data",
|
|
18
|
+
"Payments",
|
|
19
|
+
"Authentication",
|
|
20
|
+
"Storage & Files",
|
|
21
|
+
"Analytics",
|
|
22
|
+
"Social Media",
|
|
23
|
+
"Weather",
|
|
24
|
+
"Maps & Location",
|
|
25
|
+
"Translation",
|
|
26
|
+
"OCR & Vision",
|
|
27
|
+
"News",
|
|
28
|
+
"E-commerce",
|
|
29
|
+
"Other"
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const pricingModels = [
|
|
33
|
+
{ value: "free", label: "Free", description: "Completely free to use" },
|
|
34
|
+
{ value: "freemium", label: "Freemium", description: "Free tier + paid plans" },
|
|
35
|
+
{ value: "paid", label: "Paid", description: "Paid only (trial available)" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
type FormData = {
|
|
39
|
+
// Step 1: Provider Info
|
|
40
|
+
providerName: string;
|
|
41
|
+
email: string;
|
|
42
|
+
website: string;
|
|
43
|
+
|
|
44
|
+
// Step 2: API Details
|
|
45
|
+
apiName: string;
|
|
46
|
+
description: string;
|
|
47
|
+
category: string;
|
|
48
|
+
|
|
49
|
+
// Step 3: Technical
|
|
50
|
+
openApiUrl: string;
|
|
51
|
+
docsUrl: string;
|
|
52
|
+
|
|
53
|
+
// Step 4: Pricing
|
|
54
|
+
pricingModel: string;
|
|
55
|
+
pricingNotes: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default function RegisterPage() {
|
|
59
|
+
const [isDark, setIsDark] = useState(false);
|
|
60
|
+
const [step, setStep] = useState(1);
|
|
61
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
62
|
+
const [isComplete, setIsComplete] = useState(false);
|
|
63
|
+
const [parseStatus, setParseStatus] = useState<'idle' | 'parsing' | 'success' | 'error'>('idle');
|
|
64
|
+
const [error, setError] = useState<string | null>(null);
|
|
65
|
+
|
|
66
|
+
const [formData, setFormData] = useState<FormData>({
|
|
67
|
+
providerName: "",
|
|
68
|
+
email: "",
|
|
69
|
+
website: "",
|
|
70
|
+
apiName: "",
|
|
71
|
+
description: "",
|
|
72
|
+
category: "",
|
|
73
|
+
openApiUrl: "",
|
|
74
|
+
docsUrl: "",
|
|
75
|
+
pricingModel: "freemium",
|
|
76
|
+
pricingNotes: "",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const saved = localStorage.getItem('theme');
|
|
81
|
+
const prefersDark = saved ? saved === 'dark' : true;
|
|
82
|
+
setIsDark(prefersDark);
|
|
83
|
+
document.documentElement.classList.toggle('dark', prefersDark);
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const toggleTheme = () => {
|
|
87
|
+
const newTheme = !isDark;
|
|
88
|
+
setIsDark(newTheme);
|
|
89
|
+
document.documentElement.classList.toggle('dark', newTheme);
|
|
90
|
+
localStorage.setItem('theme', newTheme ? 'dark' : 'light');
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const updateField = (field: keyof FormData, value: string) => {
|
|
94
|
+
setFormData(prev => ({ ...prev, [field]: value }));
|
|
95
|
+
setError(null);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const parseOpenApiSpec = async () => {
|
|
99
|
+
if (!formData.openApiUrl) return;
|
|
100
|
+
|
|
101
|
+
setParseStatus('parsing');
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(formData.openApiUrl);
|
|
104
|
+
if (!response.ok) throw new Error('Could not fetch spec');
|
|
105
|
+
|
|
106
|
+
const spec = await response.json();
|
|
107
|
+
|
|
108
|
+
// Extract info from OpenAPI spec
|
|
109
|
+
if (spec.info) {
|
|
110
|
+
if (spec.info.title && !formData.apiName) {
|
|
111
|
+
updateField('apiName', spec.info.title);
|
|
112
|
+
}
|
|
113
|
+
if (spec.info.description && !formData.description) {
|
|
114
|
+
updateField('description', spec.info.description.slice(0, 500));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setParseStatus('success');
|
|
119
|
+
setTimeout(() => setParseStatus('idle'), 3000);
|
|
120
|
+
} catch {
|
|
121
|
+
setParseStatus('error');
|
|
122
|
+
setTimeout(() => setParseStatus('idle'), 3000);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const validateStep = (stepNum: number): boolean => {
|
|
127
|
+
switch (stepNum) {
|
|
128
|
+
case 1:
|
|
129
|
+
if (!formData.providerName.trim()) {
|
|
130
|
+
setError("Provider name is required");
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (!formData.email.trim() || !formData.email.includes('@')) {
|
|
134
|
+
setError("Valid email is required");
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
case 2:
|
|
139
|
+
if (!formData.apiName.trim()) {
|
|
140
|
+
setError("API name is required");
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (!formData.description.trim()) {
|
|
144
|
+
setError("Description is required");
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (!formData.category) {
|
|
148
|
+
setError("Please select a category");
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
case 3:
|
|
153
|
+
// OpenAPI URL is optional
|
|
154
|
+
return true;
|
|
155
|
+
case 4:
|
|
156
|
+
return true;
|
|
157
|
+
default:
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const nextStep = () => {
|
|
163
|
+
if (validateStep(step)) {
|
|
164
|
+
setStep(prev => Math.min(prev + 1, 4));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const prevStep = () => {
|
|
169
|
+
setStep(prev => Math.max(prev - 1, 1));
|
|
170
|
+
setError(null);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleSubmit = async () => {
|
|
174
|
+
setIsSubmitting(true);
|
|
175
|
+
setError(null);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// Submit to Convex
|
|
179
|
+
const response = await fetch(`${process.env.NEXT_PUBLIC_CONVEX_URL || 'https://brilliant-puffin-712.eu-west-1.convex.cloud'}/api/mutation`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
path: 'providers:registerProvider',
|
|
184
|
+
args: {
|
|
185
|
+
provider: {
|
|
186
|
+
name: formData.providerName,
|
|
187
|
+
email: formData.email,
|
|
188
|
+
website: formData.website,
|
|
189
|
+
},
|
|
190
|
+
api: {
|
|
191
|
+
name: formData.apiName,
|
|
192
|
+
description: formData.description,
|
|
193
|
+
category: formData.category,
|
|
194
|
+
openApiUrl: formData.openApiUrl || undefined,
|
|
195
|
+
docsUrl: formData.docsUrl || undefined,
|
|
196
|
+
pricingModel: formData.pricingModel,
|
|
197
|
+
pricingNotes: formData.pricingNotes || undefined,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
const errorData = await response.json().catch(() => ({}));
|
|
205
|
+
throw new Error(errorData.message || 'Submission failed');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Send confirmation email via Symbot SMTP
|
|
209
|
+
await fetch('https://nordsym.app.n8n.cloud/webhook/symbot-gmail', {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { 'Content-Type': 'application/json' },
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
action: 'smtp',
|
|
214
|
+
to: formData.email,
|
|
215
|
+
subject: `🦞 Your API "${formData.apiName}" is now listed on APIClaw`,
|
|
216
|
+
message: `
|
|
217
|
+
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 40px 20px;">
|
|
218
|
+
<div style="text-align: center; margin-bottom: 32px;">
|
|
219
|
+
<span style="font-size: 48px;">🦞</span>
|
|
220
|
+
<h1 style="margin: 16px 0 8px; font-size: 24px; font-weight: 700;">Welcome to APIClaw!</h1>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<p style="color: #525252; font-size: 16px; line-height: 1.6;">
|
|
224
|
+
Hey there,
|
|
225
|
+
</p>
|
|
226
|
+
|
|
227
|
+
<p style="color: #525252; font-size: 16px; line-height: 1.6;">
|
|
228
|
+
<strong>${formData.apiName}</strong> has been submitted to APIClaw and is now discoverable by AI agents.
|
|
229
|
+
</p>
|
|
230
|
+
|
|
231
|
+
<div style="background: #f5f5f5; border-radius: 12px; padding: 24px; margin: 24px 0;">
|
|
232
|
+
<h3 style="margin: 0 0 16px; font-size: 16px;">What's next?</h3>
|
|
233
|
+
<ul style="color: #525252; margin: 0; padding-left: 20px;">
|
|
234
|
+
<li style="margin-bottom: 8px;">Your API is listed in our registry</li>
|
|
235
|
+
<li style="margin-bottom: 8px;">Agents can now discover it via MCP</li>
|
|
236
|
+
<li>We'll notify you when agents start finding your API</li>
|
|
237
|
+
</ul>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<p style="color: #525252; font-size: 16px; line-height: 1.6;">
|
|
241
|
+
Questions? Reply to this email or reach out on <a href="https://twitter.com/nordsym" style="color: #ef4444;">Twitter</a>.
|
|
242
|
+
</p>
|
|
243
|
+
|
|
244
|
+
<p style="color: #737373; font-size: 14px; margin-top: 32px;">
|
|
245
|
+
— The APIClaw Team<br>
|
|
246
|
+
<a href="https://apiclaw.nordsym.com" style="color: #ef4444;">apiclaw.nordsym.com</a>
|
|
247
|
+
</p>
|
|
248
|
+
</div>
|
|
249
|
+
`
|
|
250
|
+
})
|
|
251
|
+
}).catch(() => {
|
|
252
|
+
// Email failure shouldn't block success
|
|
253
|
+
console.log('Email notification failed, but registration succeeded');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
setIsComplete(true);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
setError(err instanceof Error ? err.message : 'Something went wrong. Please try again.');
|
|
259
|
+
} finally {
|
|
260
|
+
setIsSubmitting(false);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Success screen
|
|
265
|
+
if (isComplete) {
|
|
266
|
+
return (
|
|
267
|
+
<main className="min-h-screen flex items-center justify-center px-6">
|
|
268
|
+
<div className="max-w-md w-full text-center">
|
|
269
|
+
<div className="w-20 h-20 rounded-full bg-accent/20 flex items-center justify-center mx-auto mb-8">
|
|
270
|
+
<Check className="w-10 h-10 text-accent" />
|
|
271
|
+
</div>
|
|
272
|
+
<h1 className="text-3xl font-bold mb-4">You're Listed! 🦞</h1>
|
|
273
|
+
<p className="text-text-secondary mb-8">
|
|
274
|
+
<strong>{formData.apiName}</strong> is now discoverable by AI agents on APIClaw.
|
|
275
|
+
We've sent a confirmation to <strong>{formData.email}</strong>.
|
|
276
|
+
</p>
|
|
277
|
+
<div className="space-y-4">
|
|
278
|
+
<Link href="/" className="btn-primary w-full justify-center">
|
|
279
|
+
<Sparkles className="w-5 h-5" />
|
|
280
|
+
Explore APIClaw
|
|
281
|
+
</Link>
|
|
282
|
+
<Link href="/providers" className="btn-secondary w-full justify-center">
|
|
283
|
+
Back to Provider Hub
|
|
284
|
+
</Link>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</main>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<main className="min-h-screen">
|
|
293
|
+
{/* Header */}
|
|
294
|
+
<header className="fixed top-0 w-full z-50 bg-background/90 backdrop-blur-xl border-b border-border-subtle">
|
|
295
|
+
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
296
|
+
<Link href="/" className="flex items-center gap-3">
|
|
297
|
+
<div className="w-9 h-9 rounded-xl bg-accent/20 flex items-center justify-center text-xl">
|
|
298
|
+
🦞
|
|
299
|
+
</div>
|
|
300
|
+
<span className="font-bold text-lg tracking-tight">APIClaw</span>
|
|
301
|
+
</Link>
|
|
302
|
+
<div className="flex items-center gap-3">
|
|
303
|
+
<button
|
|
304
|
+
onClick={toggleTheme}
|
|
305
|
+
className="p-2 rounded-lg hover:bg-[var(--surface)] transition"
|
|
306
|
+
aria-label="Toggle theme"
|
|
307
|
+
>
|
|
308
|
+
{isDark ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
|
309
|
+
</button>
|
|
310
|
+
<a
|
|
311
|
+
href="https://github.com/nordsym/apiclaw"
|
|
312
|
+
target="_blank"
|
|
313
|
+
rel="noopener noreferrer"
|
|
314
|
+
className="btn-secondary !py-2 !px-4 text-sm"
|
|
315
|
+
>
|
|
316
|
+
<Github className="w-4 h-4" />
|
|
317
|
+
</a>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
</header>
|
|
321
|
+
|
|
322
|
+
<div className="pt-32 pb-24 px-6">
|
|
323
|
+
<div className="max-w-xl mx-auto">
|
|
324
|
+
{/* Progress */}
|
|
325
|
+
<div className="mb-12">
|
|
326
|
+
<div className="flex items-center justify-between mb-4">
|
|
327
|
+
{[1, 2, 3, 4].map((s) => (
|
|
328
|
+
<div key={s} className="flex items-center">
|
|
329
|
+
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold transition-all ${
|
|
330
|
+
s < step ? 'bg-accent text-background' :
|
|
331
|
+
s === step ? 'bg-accent/20 text-accent border-2 border-accent' :
|
|
332
|
+
'bg-surface border border-border text-text-muted'
|
|
333
|
+
}`}>
|
|
334
|
+
{s < step ? <Check className="w-5 h-5" /> : s}
|
|
335
|
+
</div>
|
|
336
|
+
{s < 4 && (
|
|
337
|
+
<div className={`w-16 md:w-24 h-1 mx-2 rounded ${
|
|
338
|
+
s < step ? 'bg-accent' : 'bg-border'
|
|
339
|
+
}`} />
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
))}
|
|
343
|
+
</div>
|
|
344
|
+
<div className="text-center">
|
|
345
|
+
<span className="text-sm text-text-muted">
|
|
346
|
+
Step {step} of 4: {
|
|
347
|
+
step === 1 ? "Provider Info" :
|
|
348
|
+
step === 2 ? "API Details" :
|
|
349
|
+
step === 3 ? "Technical" :
|
|
350
|
+
"Pricing"
|
|
351
|
+
}
|
|
352
|
+
</span>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
{/* Form */}
|
|
357
|
+
<div className="rounded-2xl bg-surface-elevated border border-border p-8">
|
|
358
|
+
{/* Step 1: Provider Info */}
|
|
359
|
+
{step === 1 && (
|
|
360
|
+
<div className="space-y-6">
|
|
361
|
+
<div className="text-center mb-8">
|
|
362
|
+
<h2 className="text-2xl font-bold mb-2">Tell us about you</h2>
|
|
363
|
+
<p className="text-text-secondary">Who's listing this API?</p>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<div>
|
|
367
|
+
<label className="block text-sm font-medium mb-2">Company / Provider Name *</label>
|
|
368
|
+
<input
|
|
369
|
+
type="text"
|
|
370
|
+
value={formData.providerName}
|
|
371
|
+
onChange={(e) => updateField('providerName', e.target.value)}
|
|
372
|
+
placeholder="Acme Inc"
|
|
373
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
374
|
+
/>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<div>
|
|
378
|
+
<label className="block text-sm font-medium mb-2">Email *</label>
|
|
379
|
+
<input
|
|
380
|
+
type="email"
|
|
381
|
+
value={formData.email}
|
|
382
|
+
onChange={(e) => updateField('email', e.target.value)}
|
|
383
|
+
placeholder="you@company.com"
|
|
384
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
385
|
+
/>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<div>
|
|
389
|
+
<label className="block text-sm font-medium mb-2">Website</label>
|
|
390
|
+
<input
|
|
391
|
+
type="url"
|
|
392
|
+
value={formData.website}
|
|
393
|
+
onChange={(e) => updateField('website', e.target.value)}
|
|
394
|
+
placeholder="https://company.com"
|
|
395
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
396
|
+
/>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
400
|
+
|
|
401
|
+
{/* Step 2: API Details */}
|
|
402
|
+
{step === 2 && (
|
|
403
|
+
<div className="space-y-6">
|
|
404
|
+
<div className="text-center mb-8">
|
|
405
|
+
<h2 className="text-2xl font-bold mb-2">API Details</h2>
|
|
406
|
+
<p className="text-text-secondary">Help agents understand your API</p>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
<div>
|
|
410
|
+
<label className="block text-sm font-medium mb-2">API Name *</label>
|
|
411
|
+
<input
|
|
412
|
+
type="text"
|
|
413
|
+
value={formData.apiName}
|
|
414
|
+
onChange={(e) => updateField('apiName', e.target.value)}
|
|
415
|
+
placeholder="My Awesome API"
|
|
416
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
417
|
+
/>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<div>
|
|
421
|
+
<label className="block text-sm font-medium mb-2">Description *</label>
|
|
422
|
+
<textarea
|
|
423
|
+
value={formData.description}
|
|
424
|
+
onChange={(e) => updateField('description', e.target.value)}
|
|
425
|
+
placeholder="What does your API do? What problems does it solve?"
|
|
426
|
+
rows={4}
|
|
427
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition resize-none"
|
|
428
|
+
/>
|
|
429
|
+
<p className="text-xs text-text-muted mt-1">{formData.description.length}/500 characters</p>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<div>
|
|
433
|
+
<label className="block text-sm font-medium mb-2">Category *</label>
|
|
434
|
+
<div className="relative">
|
|
435
|
+
<Tag className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
|
|
436
|
+
<select
|
|
437
|
+
value={formData.category}
|
|
438
|
+
onChange={(e) => updateField('category', e.target.value)}
|
|
439
|
+
className="w-full pl-12 pr-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition appearance-none cursor-pointer"
|
|
440
|
+
>
|
|
441
|
+
<option value="">Select a category...</option>
|
|
442
|
+
{categories.map((cat) => (
|
|
443
|
+
<option key={cat} value={cat}>{cat}</option>
|
|
444
|
+
))}
|
|
445
|
+
</select>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
|
|
451
|
+
{/* Step 3: Technical */}
|
|
452
|
+
{step === 3 && (
|
|
453
|
+
<div className="space-y-6">
|
|
454
|
+
<div className="text-center mb-8">
|
|
455
|
+
<h2 className="text-2xl font-bold mb-2">Technical Details</h2>
|
|
456
|
+
<p className="text-text-secondary">Optional but recommended</p>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
<div>
|
|
460
|
+
<label className="block text-sm font-medium mb-2">
|
|
461
|
+
OpenAPI Spec URL
|
|
462
|
+
<span className="text-text-muted font-normal ml-2">(optional)</span>
|
|
463
|
+
</label>
|
|
464
|
+
<div className="flex gap-2">
|
|
465
|
+
<div className="relative flex-1">
|
|
466
|
+
<FileJson className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
|
|
467
|
+
<input
|
|
468
|
+
type="url"
|
|
469
|
+
value={formData.openApiUrl}
|
|
470
|
+
onChange={(e) => updateField('openApiUrl', e.target.value)}
|
|
471
|
+
placeholder="https://api.example.com/openapi.json"
|
|
472
|
+
className="w-full pl-12 pr-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
473
|
+
/>
|
|
474
|
+
</div>
|
|
475
|
+
<button
|
|
476
|
+
onClick={parseOpenApiSpec}
|
|
477
|
+
disabled={!formData.openApiUrl || parseStatus === 'parsing'}
|
|
478
|
+
className="btn-secondary !px-4 whitespace-nowrap disabled:opacity-50"
|
|
479
|
+
>
|
|
480
|
+
{parseStatus === 'parsing' ? (
|
|
481
|
+
<Loader2 className="w-5 h-5 animate-spin" />
|
|
482
|
+
) : parseStatus === 'success' ? (
|
|
483
|
+
<Check className="w-5 h-5 text-green-500" />
|
|
484
|
+
) : parseStatus === 'error' ? (
|
|
485
|
+
<AlertCircle className="w-5 h-5 text-red-500" />
|
|
486
|
+
) : (
|
|
487
|
+
'Parse'
|
|
488
|
+
)}
|
|
489
|
+
</button>
|
|
490
|
+
</div>
|
|
491
|
+
<p className="text-xs text-text-muted mt-1">
|
|
492
|
+
We'll auto-fill API details if you provide a spec
|
|
493
|
+
</p>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<div>
|
|
497
|
+
<label className="block text-sm font-medium mb-2">
|
|
498
|
+
Documentation URL
|
|
499
|
+
<span className="text-text-muted font-normal ml-2">(optional)</span>
|
|
500
|
+
</label>
|
|
501
|
+
<div className="relative">
|
|
502
|
+
<Globe className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
|
|
503
|
+
<input
|
|
504
|
+
type="url"
|
|
505
|
+
value={formData.docsUrl}
|
|
506
|
+
onChange={(e) => updateField('docsUrl', e.target.value)}
|
|
507
|
+
placeholder="https://docs.example.com"
|
|
508
|
+
className="w-full pl-12 pr-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
509
|
+
/>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<div className="bg-surface rounded-xl p-4 border border-border">
|
|
514
|
+
<p className="text-sm text-text-secondary">
|
|
515
|
+
<strong className="text-text-primary">💡 Pro tip:</strong> APIs with OpenAPI specs get better placement in agent search results.
|
|
516
|
+
</p>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
)}
|
|
520
|
+
|
|
521
|
+
{/* Step 4: Pricing */}
|
|
522
|
+
{step === 4 && (
|
|
523
|
+
<div className="space-y-6">
|
|
524
|
+
<div className="text-center mb-8">
|
|
525
|
+
<h2 className="text-2xl font-bold mb-2">Pricing Model</h2>
|
|
526
|
+
<p className="text-text-secondary">How do you charge for your API?</p>
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
<div className="space-y-3">
|
|
530
|
+
{pricingModels.map((model) => (
|
|
531
|
+
<label
|
|
532
|
+
key={model.value}
|
|
533
|
+
className={`block p-4 rounded-xl border cursor-pointer transition ${
|
|
534
|
+
formData.pricingModel === model.value
|
|
535
|
+
? 'border-accent bg-accent/5'
|
|
536
|
+
: 'border-border hover:border-accent/50'
|
|
537
|
+
}`}
|
|
538
|
+
>
|
|
539
|
+
<div className="flex items-center gap-3">
|
|
540
|
+
<input
|
|
541
|
+
type="radio"
|
|
542
|
+
name="pricingModel"
|
|
543
|
+
value={model.value}
|
|
544
|
+
checked={formData.pricingModel === model.value}
|
|
545
|
+
onChange={(e) => updateField('pricingModel', e.target.value)}
|
|
546
|
+
className="sr-only"
|
|
547
|
+
/>
|
|
548
|
+
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
|
549
|
+
formData.pricingModel === model.value
|
|
550
|
+
? 'border-accent'
|
|
551
|
+
: 'border-border'
|
|
552
|
+
}`}>
|
|
553
|
+
{formData.pricingModel === model.value && (
|
|
554
|
+
<div className="w-3 h-3 rounded-full bg-accent" />
|
|
555
|
+
)}
|
|
556
|
+
</div>
|
|
557
|
+
<div className="flex-1">
|
|
558
|
+
<div className="font-medium">{model.label}</div>
|
|
559
|
+
<div className="text-sm text-text-muted">{model.description}</div>
|
|
560
|
+
</div>
|
|
561
|
+
<DollarSign className={`w-5 h-5 ${
|
|
562
|
+
formData.pricingModel === model.value ? 'text-accent' : 'text-text-muted'
|
|
563
|
+
}`} />
|
|
564
|
+
</div>
|
|
565
|
+
</label>
|
|
566
|
+
))}
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<div>
|
|
570
|
+
<label className="block text-sm font-medium mb-2">
|
|
571
|
+
Pricing Notes
|
|
572
|
+
<span className="text-text-muted font-normal ml-2">(optional)</span>
|
|
573
|
+
</label>
|
|
574
|
+
<textarea
|
|
575
|
+
value={formData.pricingNotes}
|
|
576
|
+
onChange={(e) => updateField('pricingNotes', e.target.value)}
|
|
577
|
+
placeholder="e.g., Free tier: 1000 requests/month. Pro: $29/month for unlimited."
|
|
578
|
+
rows={3}
|
|
579
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition resize-none"
|
|
580
|
+
/>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
{/* Summary */}
|
|
584
|
+
<div className="bg-surface rounded-xl p-6 border border-border">
|
|
585
|
+
<h3 className="font-semibold mb-4">Summary</h3>
|
|
586
|
+
<dl className="space-y-2 text-sm">
|
|
587
|
+
<div className="flex justify-between">
|
|
588
|
+
<dt className="text-text-muted">Provider:</dt>
|
|
589
|
+
<dd className="font-medium">{formData.providerName}</dd>
|
|
590
|
+
</div>
|
|
591
|
+
<div className="flex justify-between">
|
|
592
|
+
<dt className="text-text-muted">API:</dt>
|
|
593
|
+
<dd className="font-medium">{formData.apiName}</dd>
|
|
594
|
+
</div>
|
|
595
|
+
<div className="flex justify-between">
|
|
596
|
+
<dt className="text-text-muted">Category:</dt>
|
|
597
|
+
<dd className="font-medium">{formData.category}</dd>
|
|
598
|
+
</div>
|
|
599
|
+
<div className="flex justify-between">
|
|
600
|
+
<dt className="text-text-muted">Pricing:</dt>
|
|
601
|
+
<dd className="font-medium capitalize">{formData.pricingModel}</dd>
|
|
602
|
+
</div>
|
|
603
|
+
</dl>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
)}
|
|
607
|
+
|
|
608
|
+
{/* Error */}
|
|
609
|
+
{error && (
|
|
610
|
+
<div className="mt-6 p-4 rounded-xl bg-red-500/10 border border-red-500/30 flex items-center gap-3 text-red-500">
|
|
611
|
+
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
|
612
|
+
<span className="text-sm">{error}</span>
|
|
613
|
+
</div>
|
|
614
|
+
)}
|
|
615
|
+
|
|
616
|
+
{/* Navigation */}
|
|
617
|
+
<div className="flex items-center justify-between mt-8 pt-6 border-t border-border">
|
|
618
|
+
<button
|
|
619
|
+
onClick={prevStep}
|
|
620
|
+
disabled={step === 1}
|
|
621
|
+
className="btn-secondary disabled:opacity-50 disabled:cursor-not-allowed"
|
|
622
|
+
>
|
|
623
|
+
<ArrowLeft className="w-4 h-4" />
|
|
624
|
+
Back
|
|
625
|
+
</button>
|
|
626
|
+
|
|
627
|
+
{step < 4 ? (
|
|
628
|
+
<button onClick={nextStep} className="btn-primary">
|
|
629
|
+
Continue
|
|
630
|
+
<ArrowRight className="w-4 h-4" />
|
|
631
|
+
</button>
|
|
632
|
+
) : (
|
|
633
|
+
<button
|
|
634
|
+
onClick={handleSubmit}
|
|
635
|
+
disabled={isSubmitting}
|
|
636
|
+
className="btn-primary disabled:opacity-50"
|
|
637
|
+
>
|
|
638
|
+
{isSubmitting ? (
|
|
639
|
+
<>
|
|
640
|
+
<Loader2 className="w-5 h-5 animate-spin" />
|
|
641
|
+
Submitting...
|
|
642
|
+
</>
|
|
643
|
+
) : (
|
|
644
|
+
<>
|
|
645
|
+
<Sparkles className="w-5 h-5" />
|
|
646
|
+
Submit API
|
|
647
|
+
</>
|
|
648
|
+
)}
|
|
649
|
+
</button>
|
|
650
|
+
)}
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
|
|
654
|
+
{/* Help text */}
|
|
655
|
+
<p className="text-center text-text-muted text-sm mt-6">
|
|
656
|
+
Questions?{' '}
|
|
657
|
+
<a href="mailto:gustav@nordsym.com" className="text-accent hover:underline">
|
|
658
|
+
Contact us
|
|
659
|
+
</a>
|
|
660
|
+
{' '}or check the{' '}
|
|
661
|
+
<Link href="/providers#faq" className="text-accent hover:underline inline-flex items-center gap-1">
|
|
662
|
+
FAQ
|
|
663
|
+
<ExternalLink className="w-3 h-3" />
|
|
664
|
+
</Link>
|
|
665
|
+
</p>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
</main>
|
|
669
|
+
);
|
|
670
|
+
}
|