Initial commit: Full Crawl API implementation
Some checks failed
CI / Test (push) Has been cancelled
Deploy / Deploy to Staging (push) Has been cancelled
CI / Build & Push (push) Has been cancelled
Deploy / Deploy to Production (push) Has been cancelled

This commit is contained in:
2026-04-29 07:03:48 +00:00
commit 62994d4f3d
92 changed files with 6176 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
const plans = [
{ name: 'Free', price: '$0', credits: '30 / month', features: ['9 endpoints', '1 concurrent', 'Community support'] },
{ name: 'Hobby', price: '$9', credits: '1,000 / month', features: ['9 endpoints', '3 concurrent', 'Email support', 'Webhooks'] },
{ name: 'Starter', price: '$19', credits: '3,000 / month', features: ['9 endpoints', '5 concurrent', 'Priority support', 'Webhooks', 'AI extraction'] },
{ name: 'Pro', price: '$49', credits: '10,000 / month', features: ['All endpoints', '10 concurrent', 'Priority support', 'Webhooks', 'AI extraction', 'Proxy rotation'] },
{ name: 'Startup', price: '$99', credits: '25,000 / month', features: ['All endpoints', '20 concurrent', 'Dedicated support', 'Custom integrations', 'SLA'] },
]
export default function Billing() {
const [token, setToken] = useState('')
const [credits, setCredits] = useState<number | null>(null)
const [usage, setUsage] = useState(0)
useEffect(() => {
const t = localStorage.getItem('crawlapi_token')
if (t) {
setToken(t)
fetchUser(t)
}
}, [])
async function fetchUser(t: string) {
try {
const res = await fetch('http://localhost:3000/api/auth/api-keys', {
headers: { 'x-auth-token': t }
})
// Just a mock - in production this would call a /me endpoint
setCredits(30)
setUsage(12)
} catch (e) {
console.error(e)
}
}
return (
<main style={{ maxWidth: 1200, margin: '0 auto', padding: '40px 20px' }}>
<nav style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 40 }}>
<Link href="/" style={{ fontSize: 24, fontWeight: 700, color: '#fff', textDecoration: 'none' }}>Crawl API</Link>
<Link href="/" style={{ color: '#888', textDecoration: 'none' }}> Back</Link>
</nav>
<h1 style={{ fontSize: 36, marginBottom: 8 }}>Billing</h1>
<p style={{ color: '#888', marginBottom: 40 }}>Manage your subscription and usage.</p>
{token && credits !== null && (
<div style={{ background: '#111', borderRadius: 12, padding: 24, marginBottom: 40 }}>
<h3 style={{ marginTop: 0 }}>Current Usage</h3>
<div style={{ display: 'flex', gap: 40, marginTop: 16 }}>
<div>
<div style={{ fontSize: 32, fontWeight: 700 }}>{credits - usage}</div>
<div style={{ color: '#888', fontSize: 14 }}>Credits remaining</div>
</div>
<div>
<div style={{ fontSize: 32, fontWeight: 700 }}>{usage}</div>
<div style={{ color: '#888', fontSize: 14 }}>Used this month</div>
</div>
<div>
<div style={{ fontSize: 32, fontWeight: 700 }}>{credits}</div>
<div style={{ color: '#888', fontSize: 14 }}>Total credits</div>
</div>
</div>
<div style={{ marginTop: 16, background: '#1a1a1a', borderRadius: 8, height: 8, overflow: 'hidden' }}>
<div style={{ width: `${(usage / credits) * 100}%`, background: '#4ade80', height: '100%' }} />
</div>
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16 }}>
{plans.map(plan => (
<div key={plan.name} style={{ background: '#111', borderRadius: 12, padding: 24, border: plan.name === 'Hobby' ? '1px solid #4ade80' : '1px solid transparent' }}>
<div style={{ fontSize: 14, color: '#888', marginBottom: 8 }}>{plan.name}</div>
<div style={{ fontSize: 36, fontWeight: 700, marginBottom: 8 }}>{plan.price}<span style={{ fontSize: 14, color: '#888' }}>/mo</span></div>
<div style={{ fontSize: 14, marginBottom: 16, color: '#4ade80' }}>{plan.credits}</div>
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
{plan.features.map((f, i) => (
<li key={i} style={{ padding: '4px 0', fontSize: 14, color: '#aaa' }}> {f}</li>
))}
</ul>
<button style={{ width: '100%', marginTop: 16, padding: '10px', background: plan.name === 'Hobby' ? '#4ade80' : '#fff', color: '#000', borderRadius: 6, border: 'none', fontWeight: 600, cursor: 'pointer' }}>
{plan.name === 'Free' ? 'Current Plan' : 'Upgrade'}
</button>
</div>
))}
</div>
</main>
)
}

View File

@@ -0,0 +1,320 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
export default function Dashboard() {
const [token, setToken] = useState('')
const [apiKey, setApiKey] = useState('')
const [url, setUrl] = useState('https://example.com')
const [endpoint, setEndpoint] = useState('screenshot')
const [result, setResult] = useState('')
const [loading, setLoading] = useState(false)
const [apiKeys, setApiKeys] = useState<{id: string, name: string}[]>([])
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [newKeyName, setNewKeyName] = useState('')
const endpoints = ['crawl', 'content', 'screenshot', 'pdf', 'markdown', 'snapshot', 'scrape', 'json', 'links']
useEffect(() => {
const saved = localStorage.getItem('crawlapi_token')
if (saved) {
setToken(saved)
setIsLoggedIn(true)
fetchApiKeys(saved)
}
}, [])
async function login() {
try {
const res = await fetch('http://localhost:3000/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const data = await res.json()
if (data.token) {
setToken(data.token)
setIsLoggedIn(true)
localStorage.setItem('crawlapi_token', data.token)
fetchApiKeys(data.token)
} else {
setResult(JSON.stringify(data, null, 2))
}
} catch (e) {
setResult(String(e))
}
}
async function fetchApiKeys(t: string) {
try {
const res = await fetch('http://localhost:3000/api/auth/api-keys', {
headers: { 'x-auth-token': t },
})
const data = await res.json()
setApiKeys(data || [])
} catch (e) {
console.error(e)
}
}
async function createApiKey() {
try {
const res = await fetch('http://localhost:3000/api/auth/api-keys', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-auth-token': token,
},
body: JSON.stringify({ name: newKeyName || 'New Key' }),
})
const data = await res.json()
setResult(JSON.stringify(data, null, 2))
fetchApiKeys(token)
} catch (e) {
setResult(String(e))
}
}
async function testApi() {
setLoading(true)
try {
const res = await fetch(`http://localhost:3000/api/${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
},
body: JSON.stringify({ url }),
})
const data = await res.json()
setResult(JSON.stringify(data, null, 2))
} catch (e) {
setResult(String(e))
} finally {
setLoading(false)
}
}
return (
<main style={{ maxWidth: 1200, margin: '0 auto', padding: '40px 20px' }}>
<nav style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 60 }}>
<Link href="/" style={{ fontSize: 24, fontWeight: 700, color: '#fff', textDecoration: 'none' }}>Crawl API</Link>
<Link href="/" style={{ color: '#888', textDecoration: 'none' }}> Back</Link>
</nav>
<h1 style={{ fontSize: 36, marginBottom: 8 }}>Dashboard</h1>
<p style={{ color: '#888', marginBottom: 40 }}>Test your API keys and monitor usage.</p>
{!isLoggedIn ? (
<div style={{ background: '#111', borderRadius: 12, padding: 24, marginBottom: 24 }}>
<h3 style={{ marginTop: 0 }}>Login</h3>
<button
onClick={async () => {
const res = await fetch('http://localhost:3000/api/auth/google')
const data = await res.json()
if (data.url) window.location.href = data.url
}}
style={{
background: '#4285f4',
color: '#fff',
padding: '12px 24px',
borderRadius: 8,
border: 'none',
fontWeight: 600,
cursor: 'pointer',
marginBottom: 16,
width: '100%'
}}
>
Sign in with Google
</button>
<div style={{ color: '#888', textAlign: 'center', marginBottom: 16 }}>or</div>
<div style={{ display: 'grid', gap: 16, marginBottom: 16 }}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="demo@crawlapi.dev"
style={{
width: '100%',
padding: 12,
background: '#1a1a1a',
border: '1px solid #333',
borderRadius: 8,
color: '#fff',
fontSize: 14,
boxSizing: 'border-box'
}}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="password"
style={{
width: '100%',
padding: 12,
background: '#1a1a1a',
border: '1px solid #333',
borderRadius: 8,
color: '#fff',
fontSize: 14,
boxSizing: 'border-box'
}}
/>
</div>
<button
onClick={login}
style={{
background: '#fff',
color: '#000',
padding: '12px 24px',
borderRadius: 8,
border: 'none',
fontWeight: 600,
cursor: 'pointer'
}}
>
Login
</button>
</div>
) : (
<>
<div style={{ background: '#111', borderRadius: 12, padding: 24, marginBottom: 24 }}>
<h3 style={{ marginTop: 0 }}>Your API Keys</h3>
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
<input
type="text"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="Key name"
style={{
flex: 1,
padding: 12,
background: '#1a1a1a',
border: '1px solid #333',
borderRadius: 8,
color: '#fff',
fontSize: 14,
}}
/>
<button
onClick={createApiKey}
style={{
background: '#fff',
color: '#000',
padding: '12px 24px',
borderRadius: 8,
border: 'none',
fontWeight: 600,
cursor: 'pointer'
}}
>
Create Key
</button>
</div>
{apiKeys.length > 0 ? (
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
{apiKeys.map((k) => (
<li key={k.id} style={{ padding: '8px 0', borderBottom: '1px solid #222', color: '#888' }}>
{k.name} <span style={{ fontFamily: 'monospace', fontSize: 12 }}>({k.id})</span>
</li>
))}
</ul>
) : (
<p style={{ color: '#888' }}>No API keys yet. Create one above.</p>
)}
</div>
<div style={{ background: '#111', borderRadius: 12, padding: 24, marginBottom: 24 }}>
<h3 style={{ marginTop: 0 }}>Test API</h3>
<div style={{ display: 'grid', gap: 16, marginBottom: 16 }}>
<div>
<label style={{ display: 'block', color: '#888', marginBottom: 8, fontSize: 14 }}>API Key</label>
<input
type="text"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="your-api-key"
style={{
width: '100%',
padding: 12,
background: '#1a1a1a',
border: '1px solid #333',
borderRadius: 8,
color: '#fff',
fontSize: 14,
boxSizing: 'border-box'
}}
/>
</div>
<div>
<label style={{ display: 'block', color: '#888', marginBottom: 8, fontSize: 14 }}>URL</label>
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
style={{
width: '100%',
padding: 12,
background: '#1a1a1a',
border: '1px solid #333',
borderRadius: 8,
color: '#fff',
fontSize: 14,
boxSizing: 'border-box'
}}
/>
</div>
<div>
<label style={{ display: 'block', color: '#888', marginBottom: 8, fontSize: 14 }}>Endpoint</label>
<select
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
style={{
width: '100%',
padding: 12,
background: '#1a1a1a',
border: '1px solid #333',
borderRadius: 8,
color: '#fff',
fontSize: 14
}}
>
{endpoints.map((ep) => (
<option key={ep} value={ep}>{ep}</option>
))}
</select>
</div>
</div>
<button
onClick={testApi}
disabled={loading}
style={{
background: loading ? '#333' : '#fff',
color: '#000',
padding: '12px 24px',
borderRadius: 8,
border: 'none',
fontWeight: 600,
cursor: loading ? 'not-allowed' : 'pointer'
}}
>
{loading ? 'Sending...' : 'Send Request'}
</button>
</div>
</>
)}
{result && (
<div style={{ background: '#111', borderRadius: 12, padding: 24 }}>
<h3 style={{ marginTop: 0 }}>Response</h3>
<pre style={{ margin: 0, overflow: 'auto', fontSize: 13 }}>{result}</pre>
</div>
)}
</main>
)
}

106
frontend/app/docs/page.tsx Normal file
View File

@@ -0,0 +1,106 @@
import Link from 'next/link'
export default function Docs() {
return (
<main style={{ maxWidth: 1200, margin: '0 auto', padding: '40px 20px' }}>
<nav style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 60 }}>
<Link href="/" style={{ fontSize: 24, fontWeight: 700, color: '#fff', textDecoration: 'none' }}>Crawl API</Link>
<Link href="/" style={{ color: '#888', textDecoration: 'none' }}> Back</Link>
</nav>
<h1 style={{ fontSize: 42, marginBottom: 16 }}>API Documentation</h1>
<p style={{ color: '#888', marginBottom: 48 }}>
API reference for Crawl API 9 endpoints for crawling, scraping, screenshots, PDFs, and more.
</p>
<section style={{ marginBottom: 48 }}>
<h2 style={{ fontSize: 24, marginBottom: 16 }}>Base URL</h2>
<code style={{ background: '#111', padding: '12px 16px', borderRadius: 8, display: 'block' }}>
https://crawlapi.dev
</code>
</section>
<section style={{ marginBottom: 48 }}>
<h2 style={{ fontSize: 24, marginBottom: 16 }}>Authentication</h2>
<p style={{ color: '#888', marginBottom: 12 }}>All requests require an API key sent via the x-api-key header.</p>
<code style={{ background: '#111', padding: '12px 16px', borderRadius: 8, display: 'block' }}>
x-api-key: YOUR_API_KEY
</code>
</section>
<section style={{ marginBottom: 48 }}>
<h2 style={{ fontSize: 24, marginBottom: 16 }}>Request format</h2>
<p style={{ color: '#888', marginBottom: 12 }}>Every endpoint accepts a POST request with a JSON body. The url field is always required.</p>
<pre style={{ background: '#111', padding: 16, borderRadius: 8, overflow: 'auto' }}>
{`curl -X POST https://crawlapi.dev/api/screenshot \\
-H "Content-Type: application/json" \\
-H "x-api-key: YOUR_API_KEY" \\
-d '{"url": "https://example.com"}'`}
</pre>
</section>
<section style={{ marginBottom: 48 }}>
<h2 style={{ fontSize: 24, marginBottom: 16 }}>Endpoints</h2>
{[
{ path: '/api/crawl', desc: 'Full JS-rendered page crawl' },
{ path: '/api/content', desc: 'Raw HTML content' },
{ path: '/api/screenshot', desc: 'PNG screenshot' },
{ path: '/api/pdf', desc: 'PDF export' },
{ path: '/api/markdown', desc: 'Markdown extraction' },
{ path: '/api/snapshot', desc: 'HTML + screenshot' },
{ path: '/api/scrape', desc: 'CSS selector extraction' },
{ path: '/api/json', desc: 'Structured JSON' },
{ path: '/api/links', desc: 'Extract all links' },
].map((ep) => (
<div key={ep.path} style={{ background: '#111', borderRadius: 8, padding: 16, marginBottom: 12 }}>
<span style={{ color: '#4ade80', fontWeight: 600 }}>POST</span>{' '}
<code>{ep.path}</code>
<div style={{ color: '#888', marginTop: 4 }}>{ep.desc}</div>
</div>
))}
</section>
<section style={{ marginBottom: 48 }}>
<h2 style={{ fontSize: 24, marginBottom: 16 }}>Rate limits</h2>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<tbody>
{[
{ label: 'Requests per minute', value: '60' },
{ label: 'Max concurrent', value: '10' },
{ label: 'Request timeout', value: '30s' },
].map((row) => (
<tr key={row.label} style={{ borderBottom: '1px solid #222' }}>
<td style={{ padding: '12px 0', color: '#888' }}>{row.label}</td>
<td style={{ padding: '12px 0', textAlign: 'right' }}>{row.value}</td>
</tr>
))}
</tbody>
</table>
</section>
<section>
<h2 style={{ fontSize: 24, marginBottom: 16 }}>Error handling</h2>
<pre style={{ background: '#111', padding: 16, borderRadius: 8, overflow: 'auto' }}>
{`{ "success": false, "error": "Missing or invalid API key" }`}
</pre>
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: 16 }}>
<tbody>
{[
{ code: '400', meaning: 'Missing or invalid URL / bad options' },
{ code: '401', meaning: 'Missing or invalid API key' },
{ code: '403', meaning: 'Insufficient credits' },
{ code: '405', meaning: 'Wrong HTTP method (use POST)' },
{ code: '429', meaning: 'Rate limit exceeded' },
{ code: '500', meaning: 'Server error' },
].map((row) => (
<tr key={row.code} style={{ borderBottom: '1px solid #222' }}>
<td style={{ padding: '12px 0' }}>{row.code}</td>
<td style={{ padding: '12px 0', color: '#888' }}>{row.meaning}</td>
</tr>
))}
</tbody>
</table>
</section>
</main>
)
}

18
frontend/app/layout.tsx Normal file
View File

@@ -0,0 +1,18 @@
export const metadata = {
title: 'Crawl API — Headless Browser REST API',
description: 'One API to crawl, screenshot, scrape, and extract data from any webpage. Built for developers.',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body style={{ margin: 0, fontFamily: 'system-ui, -apple-system, sans-serif', background: '#0a0a0a', color: '#fff' }}>
{children}
</body>
</html>
)
}

124
frontend/app/page.tsx Normal file
View File

@@ -0,0 +1,124 @@
import Link from 'next/link'
export default function Home() {
return (
<main style={{ maxWidth: 1200, margin: '0 auto', padding: '40px 20px' }}>
<nav style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 80 }}>
<div style={{ fontSize: 24, fontWeight: 700 }}>Crawl API</div>
<div style={{ display: 'flex', gap: 24 }}>
<Link href="/docs" style={{ color: '#888', textDecoration: 'none' }}>Docs</Link>
<Link href="/playground" style={{ color: '#888', textDecoration: 'none' }}>Playground</Link>
<Link href="/billing" style={{ color: '#888', textDecoration: 'none' }}>Pricing</Link>
<Link href="/dashboard" style={{ color: '#888', textDecoration: 'none' }}>Dashboard</Link>
</div>
</nav>
<section style={{ textAlign: 'center', marginBottom: 120 }}>
<h1 style={{ fontSize: 56, marginBottom: 20, lineHeight: 1.1 }}>
Extract, capture, and convert<br />any webpage
</h1>
<p style={{ fontSize: 20, color: '#888', maxWidth: 600, margin: '0 auto 40px' }}>
Screenshots, PDFs, markdown, structured data and more all from a single API call.
Just send a URL and get back exactly what you need.
</p>
<div style={{ display: 'flex', gap: 16, justifyContent: 'center' }}>
<Link href="/dashboard" style={{
background: '#fff',
color: '#000',
padding: '14px 28px',
borderRadius: 8,
textDecoration: 'none',
fontWeight: 600
}}>
Get started free
</Link>
<Link href="/playground" style={{
border: '1px solid #333',
color: '#fff',
padding: '14px 28px',
borderRadius: 8,
textDecoration: 'none'
}}>
API Playground
</Link>
</div>
</section>
<section style={{ marginBottom: 120 }}>
<div style={{
background: '#111',
borderRadius: 12,
padding: 24,
fontFamily: 'monospace',
fontSize: 14,
overflow: 'auto'
}}>
<div style={{ color: '#888', marginBottom: 12 }}>$ curl -X POST /api/screenshot -d {'{'}&quot;url&quot;: &quot;https://example.com&quot;{'}'}</div>
<div>{'{'} &quot;success&quot;: <span style={{ color: '#4ade80' }}>true</span>,</div>
<div> &quot;url&quot;: <span style={{ color: '#fbbf24' }}>&quot;https://cdn.crawlapi.dev/s/abc123.png&quot;</span>,</div>
<div> &quot;width&quot;: <span style={{ color: '#60a5fa' }}>1440</span> {'}'}</div>
</div>
</section>
<section style={{ marginBottom: 120 }}>
<h2 style={{ fontSize: 36, marginBottom: 16, textAlign: 'center' }}>9 endpoints, one shape</h2>
<p style={{ color: '#888', textAlign: 'center', marginBottom: 48 }}>
Every endpoint accepts the same request body. Send a URL and optional config get back exactly what you need.
</p>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: 16
}}>
{[
{ method: 'POST', path: '/api/crawl', desc: 'Full JS-rendered page crawl with all resources' },
{ method: 'POST', path: '/api/content', desc: 'Raw HTML content of any page' },
{ method: 'POST', path: '/api/screenshot', desc: 'Full-page PNG screenshot, hosted on CDN' },
{ method: 'POST', path: '/api/pdf', desc: 'PDF export of any page, hosted on CDN' },
{ method: 'POST', path: '/api/markdown', desc: 'Clean Markdown extraction from any page' },
{ method: 'POST', path: '/api/snapshot', desc: 'HTML + screenshot combined in one call' },
{ method: 'POST', path: '/api/scrape', desc: 'Structured extraction with CSS selectors' },
{ method: 'POST', path: '/api/json', desc: 'Page content as structured JSON' },
{ method: 'POST', path: '/api/links', desc: 'Extract all links from any page' },
].map((ep) => (
<div key={ep.path} style={{ background: '#111', borderRadius: 12, padding: 24 }}>
<span style={{ color: '#4ade80', fontSize: 12, fontWeight: 600 }}>{ep.method}</span>
<div style={{ fontFamily: 'monospace', fontSize: 14, margin: '8px 0' }}>{ep.path}</div>
<div style={{ color: '#888', fontSize: 14 }}>{ep.desc}</div>
</div>
))}
</div>
</section>
<section style={{ marginBottom: 120 }}>
<h2 style={{ fontSize: 36, marginBottom: 16, textAlign: 'center' }}>Simple, per-call pricing</h2>
<p style={{ color: '#888', textAlign: 'center', marginBottom: 48 }}>
Start free. Scale as you grow. Every endpoint costs 1 API call no surprises.
</p>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: 16
}}>
{[
{ name: 'Hobby', price: '$9', credits: '1,000 API calls/mo', concurrent: '3 concurrent requests' },
{ name: 'Starter', price: '$19', credits: '3,000 API calls/mo', concurrent: '5 concurrent requests' },
{ name: 'Pro', price: '$49', credits: '10,000 API calls/mo', concurrent: '10 concurrent requests' },
{ name: 'Startup', price: '$99', credits: '25,000 API calls/mo', concurrent: '20 concurrent requests' },
].map((plan) => (
<div key={plan.name} style={{ background: '#111', borderRadius: 12, padding: 24 }}>
<div style={{ fontSize: 14, color: '#888', marginBottom: 8 }}>{plan.name}</div>
<div style={{ fontSize: 36, fontWeight: 700, marginBottom: 16 }}>{plan.price}<span style={{ fontSize: 14, color: '#888' }}>/mo</span></div>
<div style={{ fontSize: 14, marginBottom: 8 }}>{plan.credits}</div>
<div style={{ fontSize: 14, color: '#888' }}>{plan.concurrent}</div>
</div>
))}
</div>
</section>
<footer style={{ textAlign: 'center', color: '#888', padding: '40px 0', borderTop: '1px solid #222' }}>
© 2026 Crawl API. Built for developers.
</footer>
</main>
)
}

View File

@@ -0,0 +1,159 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
export default function Playground() {
const [apiKey, setApiKey] = useState('')
const [url, setUrl] = useState('https://example.com')
const [endpoint, setEndpoint] = useState('screenshot')
const [options, setOptions] = useState('{}')
const [result, setResult] = useState('')
const [loading, setLoading] = useState(false)
const [codeLang, setCodeLang] = useState('curl')
const endpoints = [
{ value: 'screenshot', label: 'Screenshot' },
{ value: 'pdf', label: 'PDF' },
{ value: 'crawl', label: 'Crawl' },
{ value: 'content', label: 'Content' },
{ value: 'markdown', label: 'Markdown' },
{ value: 'json', label: 'JSON' },
{ value: 'links', label: 'Links' },
{ value: 'scrape', label: 'Scrape' },
{ value: 'snapshot', label: 'Snapshot' },
{ value: 'extract', label: 'AI Extract' },
]
async function sendRequest() {
setLoading(true)
try {
const body: any = { url }
if (options && options !== '{}') {
body.options = JSON.parse(options)
}
const res = await fetch(`http://localhost:3000/api/${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
},
body: JSON.stringify(body),
})
const data = await res.json()
setResult(JSON.stringify(data, null, 2))
} catch (e) {
setResult(String(e))
} finally {
setLoading(false)
}
}
function getCodeSnippet() {
const body = JSON.stringify({ url, options: JSON.parse(options || '{}') }, null, 2)
switch (codeLang) {
case 'curl':
return `curl -X POST http://localhost:3000/api/${endpoint} \\
-H "Content-Type: application/json" \\
-H "x-api-key: ${apiKey || 'YOUR_API_KEY'}" \\
-d '${body}'`
case 'python':
return `import requests
response = requests.post(
"http://localhost:3000/api/${endpoint}",
headers={
"Content-Type": "application/json",
"x-api-key": "${apiKey || 'YOUR_API_KEY'}"
},
json=${body.replace(/true/g, 'True').replace(/false/g, 'False').replace(/null/g, 'None')}
)
print(response.json())`
case 'javascript':
return `const response = await fetch('http://localhost:3000/api/${endpoint}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': '${apiKey || 'YOUR_API_KEY'}'
},
body: JSON.stringify(${body})
});
const data = await response.json();
console.log(data);`
default:
return ''
}
}
return (
<main style={{ maxWidth: 1200, margin: '0 auto', padding: '40px 20px' }}>
<nav style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 40 }}>
<Link href="/" style={{ fontSize: 24, fontWeight: 700, color: '#fff', textDecoration: 'none' }}>Crawl API</Link>
<Link href="/" style={{ color: '#888', textDecoration: 'none' }}> Back</Link>
</nav>
<h1 style={{ fontSize: 36, marginBottom: 8 }}>API Playground</h1>
<p style={{ color: '#888', marginBottom: 32 }}>Test any endpoint directly from the browser.</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24 }}>
<div>
<div style={{ background: '#111', borderRadius: 12, padding: 24, marginBottom: 24 }}>
<h3 style={{ marginTop: 0 }}>Request</h3>
<div style={{ display: 'grid', gap: 12 }}>
<div>
<label style={{ display: 'block', color: '#888', marginBottom: 4, fontSize: 13 }}>API Key</label>
<input type="text" value={apiKey} onChange={e => setApiKey(e.target.value)} placeholder="your-api-key"
style={{ width: '100%', padding: 10, background: '#1a1a1a', border: '1px solid #333', borderRadius: 6, color: '#fff', fontSize: 13, boxSizing: 'border-box' }} />
</div>
<div>
<label style={{ display: 'block', color: '#888', marginBottom: 4, fontSize: 13 }}>Endpoint</label>
<select value={endpoint} onChange={e => setEndpoint(e.target.value)}
style={{ width: '100%', padding: 10, background: '#1a1a1a', border: '1px solid #333', borderRadius: 6, color: '#fff', fontSize: 13 }}>
{endpoints.map(ep => <option key={ep.value} value={ep.value}>{ep.label}</option>)}
</select>
</div>
<div>
<label style={{ display: 'block', color: '#888', marginBottom: 4, fontSize: 13 }}>URL</label>
<input type="text" value={url} onChange={e => setUrl(e.target.value)}
style={{ width: '100%', padding: 10, background: '#1a1a1a', border: '1px solid #333', borderRadius: 6, color: '#fff', fontSize: 13, boxSizing: 'border-box' }} />
</div>
<div>
<label style={{ display: 'block', color: '#888', marginBottom: 4, fontSize: 13 }}>Options (JSON)</label>
<textarea value={options} onChange={e => setOptions(e.target.value)} rows={4}
style={{ width: '100%', padding: 10, background: '#1a1a1a', border: '1px solid #333', borderRadius: 6, color: '#fff', fontSize: 13, boxSizing: 'border-box', fontFamily: 'monospace' }} />
</div>
<button onClick={sendRequest} disabled={loading}
style={{ background: loading ? '#333' : '#fff', color: '#000', padding: '12px', borderRadius: 6, border: 'none', fontWeight: 600, cursor: loading ? 'not-allowed' : 'pointer' }}>
{loading ? 'Sending...' : 'Send Request'}
</button>
</div>
</div>
<div style={{ background: '#111', borderRadius: 12, padding: 24 }}>
<h3 style={{ marginTop: 0 }}>Code Snippet</h3>
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
{['curl', 'python', 'javascript'].map(l => (
<button key={l} onClick={() => setCodeLang(l)}
style={{ background: codeLang === l ? '#fff' : '#1a1a1a', color: codeLang === l ? '#000' : '#888', padding: '6px 12px', borderRadius: 4, border: 'none', fontSize: 12, cursor: 'pointer', textTransform: 'uppercase' }}>
{l}
</button>
))}
</div>
<pre style={{ margin: 0, fontSize: 12, overflow: 'auto', background: '#0a0a0a', padding: 12, borderRadius: 6 }}>{getCodeSnippet()}</pre>
</div>
</div>
<div>
<div style={{ background: '#111', borderRadius: 12, padding: 24, height: '100%' }}>
<h3 style={{ marginTop: 0 }}>Response</h3>
{result ? (
<pre style={{ margin: 0, fontSize: 13, overflow: 'auto', background: '#0a0a0a', padding: 12, borderRadius: 6, height: 'calc(100% - 40px)' }}>{result}</pre>
) : (
<div style={{ color: '#555', textAlign: 'center', padding: '40px 0' }}>Send a request to see the response</div>
)}
</div>
</div>
</div>
</main>
)
}