Initial commit: Full Crawl API implementation
This commit is contained in:
92
frontend/app/billing/page.tsx
Normal file
92
frontend/app/billing/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
320
frontend/app/dashboard/page.tsx
Normal file
320
frontend/app/dashboard/page.tsx
Normal 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
106
frontend/app/docs/page.tsx
Normal 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
18
frontend/app/layout.tsx
Normal 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
124
frontend/app/page.tsx
Normal 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 {'{'}"url": "https://example.com"{'}'}</div>
|
||||
<div>{'{'} "success": <span style={{ color: '#4ade80' }}>true</span>,</div>
|
||||
<div> "url": <span style={{ color: '#fbbf24' }}>"https://cdn.crawlapi.dev/s/abc123.png"</span>,</div>
|
||||
<div> "width": <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>
|
||||
)
|
||||
}
|
||||
159
frontend/app/playground/page.tsx
Normal file
159
frontend/app/playground/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user