sar / src /app /api-docs /page.tsx
Mafia2008's picture
feat: full CORS support + OPTIONS preflight + interactive /api-docs reference page
fdd8ef0
Raw
History Blame Contribute Delete
30.9 kB
// ═══════════════════════════════════════════════════════════════════════════
// API REFERENCE PAGE - Full documentation for all public API endpoints
// ═══════════════════════════════════════════════════════════════════════════
'use client';
import { useState } from 'react';
const BASE = typeof window !== 'undefined'
? `${window.location.protocol}//${window.location.host}`
: 'https://your-hf-space.hf.space';
// ─── colour tokens ───────────────────────────────────────────────────────────
const C = {
bg: '#0d1117',
surface: '#161b22',
border: '#30363d',
accent: '#58a6ff',
green: '#3fb950',
purple: '#bc8cff',
orange: '#ffa657',
red: '#f85149',
text: '#e6edf3',
muted: '#8b949e',
get: '#3fb950',
post: '#58a6ff',
del: '#f85149',
};
// ─── types ───────────────────────────────────────────────────────────────────
interface Param { name: string; type: string; required: boolean; desc: string; }
interface Endpoint {
method: 'GET' | 'POST' | 'DELETE';
path: string;
summary: string;
desc: string;
params?: Param[];
body?: Param[];
example?: string;
response?: string;
tag: string;
}
const ENDPOINTS: Endpoint[] = [
// ── SESSION ──────────────────────────────────────────────────────────────
{
tag: 'Session', method: 'POST', path: '/api/session',
summary: 'Create Session (Teacher)',
desc: 'Creates a new teaching session. Returns a 4-digit room code students use to join.',
body: [
{ name: 'action', type: '"create"', required: true, desc: 'Must be "create"' },
{ name: 'teacherId', type: 'string', required: true, desc: 'Unique teacher identifier' },
{ name: 'teacherName', type: 'string', required: true, desc: 'Teacher display name' },
{ name: 'sessionName', type: 'string', required: true, desc: 'Session / class name' },
{ name: 'description', type: 'string', required: false, desc: 'Optional description' },
],
example: `fetch('/api/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'create',
teacherId: 'teacher-001',
teacherName: 'Mr. Sharma',
sessionName: 'Physics – Chapter 5'
})
})`,
response: `{ "success": true, "data": { "id": "uuid", "roomCode": "4821", "isActive": true, ... } }`,
},
{
tag: 'Session', method: 'POST', path: '/api/session',
summary: 'Lookup Session by Room Code',
desc: 'Find a session using the 4-digit room code. Call this before joining.',
body: [
{ name: 'action', type: '"lookup"', required: true, desc: 'Must be "lookup"' },
{ name: 'roomCode', type: 'string', required: true, desc: '4-digit room code' },
],
example: `fetch('/api/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'lookup', roomCode: '4821' })
})`,
response: `{ "success": true, "data": { "id": "uuid", "roomCode": "4821", ... } }`,
},
{
tag: 'Session', method: 'POST', path: '/api/session',
summary: 'Join Session (Student)',
desc: 'Student joins a session. Returns current slide, active poll, and session state.',
body: [
{ name: 'action', type: '"join"', required: true, desc: 'Must be "join"' },
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID (or 4-digit room code)' },
{ name: 'userId', type: 'string', required: true, desc: 'Unique student ID' },
{ name: 'userName', type: 'string', required: true, desc: 'Student display name' },
],
example: `fetch('/api/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'join',
sessionId: '4821', // room code also accepted
userId: 'student-xyz',
userName: 'Rahul Kumar'
})
})`,
response: `{ "success": true, "data": { "user": {...}, "currentSlide": 0, "activePoll": null, ... } }`,
},
{
tag: 'Session', method: 'POST', path: '/api/session',
summary: 'End Session (Teacher)',
desc: 'Ends the session and broadcasts a session_ended SSE event to all connected students.',
body: [
{ name: 'action', type: '"end"', required: true, desc: 'Must be "end"' },
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID to end' },
],
example: `fetch('/api/session', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'end', sessionId: 'uuid' }) })`,
response: `{ "success": true, "message": "Session ended successfully" }`,
},
{
tag: 'Session', method: 'POST', path: '/api/session',
summary: 'List All Active Sessions',
desc: 'Returns all active sessions.',
body: [{ name: 'action', type: '"list"', required: true, desc: 'Must be "list"' }],
example: `fetch('/api/session', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'list' }) })`,
response: `{ "success": true, "data": [ { "id": "...", "roomCode": "4821", ... } ] }`,
},
// ── SSE EVENTS ───────────────────────────────────────────────────────────
{
tag: 'Events (SSE)', method: 'GET', path: '/api/events',
summary: 'Subscribe to Real-time Events',
desc: `Opens a Server-Sent Events (SSE) stream. Both teachers and students connect here to receive live updates.
**SSE Events emitted:**
| Event | Sent when |
|---|---|
| \`connected\` | Stream opens (includes full session state) |
| \`slide_change\` | Teacher changes slide (includes imageBase64) |
| \`new_poll\` | Teacher creates a poll |
| \`poll_update\` | A student votes |
| \`poll_ended\` | Teacher ends poll |
| \`correct_answer\` | Teacher reveals correct answer |
| \`new_doubt\` | Student submits a doubt |
| \`doubt_solved\` | Teacher resolves a doubt |
| \`student_joined\` | A student joins |
| \`session_ended\` | Teacher ends session |`,
params: [
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
{ name: 'userId', type: 'string', required: true, desc: 'Your user ID' },
],
example: `const url = '/api/events?sessionId=UUID&userId=STUDENT_ID';
const es = new EventSource(url);
es.addEventListener('slide_change', e => {
const { slideNumber, imageBase64 } = JSON.parse(e.data);
imgEl.src = imageBase64;
});
es.addEventListener('new_poll', e => {
const poll = JSON.parse(e.data);
showPoll(poll);
});
es.addEventListener('correct_answer', e => {
const { pollId, correctOptionIds } = JSON.parse(e.data);
highlightAnswer(correctOptionIds);
});
es.addEventListener('new_doubt', e => {
// Teacher-side: student sent a doubt
const doubt = JSON.parse(e.data);
addDoubtCard(doubt);
});`,
response: `text/event-stream — raw SSE`,
},
// ── SLIDE ────────────────────────────────────────────────────────────────
{
tag: 'Slide', method: 'POST', path: '/api/slide',
summary: 'Upload + Broadcast Slide (Teacher)',
desc: 'Upload a base64 slide image. Automatically broadcasts slide_change SSE to all students with the image.',
body: [
{ name: 'action', type: '"upload"', required: true, desc: 'Must be "upload"' },
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
{ name: 'slideImage', type: 'string', required: true, desc: 'Base64 image (data:image/png;base64,... or raw base64)' },
{ name: 'slideNumber', type: 'number', required: false, desc: 'Slide index to replace (appends if omitted)' },
],
example: `// In Electron / SlideMap app:
const base64 = canvas.toDataURL('image/png');
fetch('/api/slide', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'upload',
sessionId: 'uuid',
slideImage: base64,
slideNumber: 3 // optional – omit to append
})
})`,
response: `{ "success": true, "data": { "slideNumber": 3, "totalSlides": 4 } }`,
},
{
tag: 'Slide', method: 'POST', path: '/api/slide',
summary: 'Go to Slide (Teacher)',
desc: 'Change the current slide without uploading a new image.',
body: [
{ name: 'action', type: '"goto"', required: true, desc: 'Must be "goto"' },
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
{ name: 'slideNumber', type: 'number', required: true, desc: '0-indexed slide number' },
],
example: `fetch('/api/slide', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'goto', sessionId: 'uuid', slideNumber: 2 }) })`,
response: `{ "success": true, "data": { "slideNumber": 2, "totalSlides": 5 } }`,
},
// ── POLL ─────────────────────────────────────────────────────────────────
{
tag: 'Poll', method: 'POST', path: '/api/poll',
summary: 'Create Poll (Teacher)',
desc: 'Create a new poll. Any existing active poll is auto-ended. Broadcasts new_poll SSE to all students.',
body: [
{ name: 'action', type: '"create"', required: true, desc: 'Must be "create"' },
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
{ name: 'question', type: 'string', required: true, desc: 'Poll question text' },
{ name: 'options', type: 'string[]', required: true, desc: 'Array of option strings (min 2)' },
{ name: 'type', type: '"single"|"multiple"', required: false, desc: 'Vote type (default: "single")' },
{ name: 'correctAnswer', type: 'string', required: false, desc: 'Correct option text (for quizzes)' },
{ name: 'duration', type: 'number', required: false, desc: 'Auto-end after N seconds (0 = manual)' },
],
example: `fetch('/api/poll', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'create',
sessionId: 'uuid',
question: 'What is Newton\\'s 2nd Law?',
options: ['F=ma', 'E=mc²', 'PV=nRT', 'V=IR'],
correctAnswer: 'F=ma',
duration: 0 // 0 = no timer
})
})`,
response: `{ "success": true, "data": { "id": "uuid", "question": "...", "options": [...], "isActive": true } }`,
},
{
tag: 'Poll', method: 'POST', path: '/api/poll',
summary: 'Vote on Poll (Student)',
desc: 'Student submits their answer. Each student can vote only once. Broadcasts poll_update SSE.',
body: [
{ name: 'action', type: '"vote"', required: true, desc: 'Must be "vote"' },
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
{ name: 'pollId', type: 'string', required: true, desc: 'Poll ID' },
{ name: 'option', type: 'string', required: true, desc: 'Option text the student selected' },
{ name: 'studentId', type: 'string', required: true, desc: 'Student unique ID' },
],
example: `fetch('/api/poll', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'vote',
sessionId: 'uuid',
pollId: 'poll-uuid',
option: 'F=ma',
studentId: 'student-xyz'
})
})`,
response: `{ "success": true, "data": { "pollId": "...", "results": {"F=ma":5,"E=mc²":2}, "totalVotes": 7 } }`,
},
{
tag: 'Poll', method: 'POST', path: '/api/poll',
summary: 'End Poll (Teacher)',
desc: 'Ends active poll and broadcasts poll_ended SSE with final results.',
body: [
{ name: 'action', type: '"end"', required: true, desc: 'Must be "end"' },
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
{ name: 'pollId', type: 'string', required: true, desc: 'Poll ID to end' },
],
example: `fetch('/api/poll', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'end', sessionId: 'uuid', pollId: 'poll-uuid' }) })`,
response: `{ "success": true, "message": "Poll ended successfully" }`,
},
{
tag: 'Poll', method: 'POST', path: '/api/poll',
summary: 'Reveal Correct Answer (Teacher)',
desc: 'Broadcasts correct_answer SSE so all students see the highlighted answer.',
body: [
{ name: 'action', type: '"setCorrectAnswers"', required: true, desc: 'Must be "setCorrectAnswers"' },
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
{ name: 'pollId', type: 'string', required: true, desc: 'Poll ID' },
{ name: 'correctOptionIds', type: 'string[]', required: false, desc: 'Array of correct option id strings' },
{ name: 'correctAnswer', type: 'string', required: false, desc: 'Correct option text' },
],
example: `fetch('/api/poll', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'setCorrectAnswers', sessionId: 'uuid',
pollId: 'poll-uuid', correctAnswer: 'F=ma'
}) })`,
response: `{ "success": true, "message": "Correct answers broadcast to students" }`,
},
{
tag: 'Poll', method: 'GET', path: '/api/poll',
summary: 'Get All Polls (Teacher/Student)',
desc: 'Returns all polls (active and ended) for a session.',
params: [
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
],
example: `fetch('/api/poll?sessionId=uuid')`,
response: `{ "success": true, "data": { "polls": [...], "activePoll": {...}|null, "totalPolls": 3 } }`,
},
// ── DOUBT ─────────────────────────────────────────────────────────────────
{
tag: 'Doubt', method: 'POST', path: '/api/doubt',
summary: 'Submit Doubt (Student)',
desc: 'Student submits a doubt/question. Broadcasts new_doubt SSE to the teacher.',
body: [
{ name: 'action', type: '"create"', required: true, desc: 'Must be "create"' },
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
{ name: 'studentId', type: 'string', required: true, desc: 'Student unique ID' },
{ name: 'studentName', type: 'string', required: true, desc: 'Student display name' },
{ name: 'question', type: 'string', required: true, desc: 'The doubt text' },
{ name: 'image', type: 'string', required: false, desc: 'Optional base64 image (data:image/...)' },
{ name: 'slideNumber', type: 'number', required: false, desc: 'Slide where doubt occurred' },
],
example: `fetch('/api/doubt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'create',
sessionId: 'uuid',
studentId: 'student-xyz',
studentName: 'Rahul Kumar',
question: 'I don\\'t understand why F=ma and not F=mv',
slideNumber: 3
})
})`,
response: `{ "success": true, "data": { "id": "uuid", "status": "pending", "question": "..." } }`,
},
{
tag: 'Doubt', method: 'POST', path: '/api/doubt',
summary: 'Resolve Doubt (Teacher)',
desc: 'Teacher marks doubt as solved. Broadcasts doubt_solved SSE to student.',
body: [
{ name: 'action', type: '"solve"', required: true, desc: 'Must be "solve"' },
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
{ name: 'doubtId', type: 'string', required: true, desc: 'Doubt ID to resolve' },
{ name: 'teacherResponse', type: 'string', required: false, desc: 'Text explanation from teacher' },
{ name: 'annotatedImage', type: 'string', required: false, desc: 'Base64 annotated image' },
],
example: `fetch('/api/doubt', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'solve', sessionId: 'uuid', doubtId: 'doubt-uuid',
teacherResponse: 'Because acceleration is the rate of change of velocity, not velocity itself.'
}) })`,
response: `{ "success": true, "data": { "id": "...", "status": "solved", "teacherResponse": "..." } }`,
},
{
tag: 'Doubt', method: 'GET', path: '/api/doubt',
summary: 'Get All Doubts (Teacher)',
desc: 'Returns all doubts grouped by status (pending / in_progress / solved).',
params: [{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' }],
example: `fetch('/api/doubt?sessionId=uuid')`,
response: `{ "success": true, "data": { "doubts": [...], "summary": { "pending": 2, "solved": 5 } } }`,
},
// ── CHAT ─────────────────────────────────────────────────────────────────
{
tag: 'Chat', method: 'POST', path: '/api/chat',
summary: 'Send Chat Message',
desc: 'Send a chat message. Broadcasts chat_message SSE to all participants.',
body: [
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
{ name: 'userId', type: 'string', required: true, desc: 'Sender unique ID' },
{ name: 'userName', type: 'string', required: true, desc: 'Sender display name' },
{ name: 'message', type: 'string', required: true, desc: 'Message content' },
{ name: 'isTeacher', type: 'boolean', required: false, desc: 'Flag if sender is teacher' },
],
example: `fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: 'uuid', userId: 'student-xyz',
userName: 'Rahul', message: 'Can you explain again?', isTeacher: false
}) })`,
response: `{ "success": true, "data": { "id": "...", "message": "...", "timestamp": "..." } }`,
},
{
tag: 'Chat', method: 'GET', path: '/api/chat',
summary: 'Get Chat History',
desc: 'Returns the last N chat messages for a session.',
params: [
{ name: 'sessionId', type: 'string', required: true, desc: 'Session ID' },
{ name: 'limit', type: 'number', required: false, desc: 'Max messages to return (default 100)' },
],
example: `fetch('/api/chat?sessionId=uuid&limit=50')`,
response: `{ "success": true, "data": { "messages": [...], "count": 23 } }`,
},
];
const TAGS = ['Session', 'Events (SSE)', 'Slide', 'Poll', 'Doubt', 'Chat'];
const METHOD_COLOR: Record<string, string> = { GET: C.green, POST: C.accent, DELETE: C.red };
export default function ApiDocsPage() {
const [activeTag, setActiveTag] = useState<string>('Session');
const [openIdx, setOpenIdx] = useState<number | null>(null);
const [copied, setCopied] = useState<string>('');
const filtered = ENDPOINTS.filter(e => e.tag === activeTag);
const copy = (text: string, key: string) => {
navigator.clipboard.writeText(text);
setCopied(key);
setTimeout(() => setCopied(''), 1500);
};
return (
<div style={{ background: C.bg, minHeight: '100vh', fontFamily: "'Inter', system-ui, sans-serif", color: C.text }}>
{/* ── Header ── */}
<div style={{
background: `linear-gradient(135deg, #1a1f2e 0%, #0d1117 100%)`,
borderBottom: `1px solid ${C.border}`,
padding: '40px 0 0',
}}>
<div style={{ maxWidth: 900, margin: '0 auto', padding: '0 24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
<div style={{
background: 'linear-gradient(135deg, #58a6ff, #bc8cff)',
borderRadius: 10, width: 40, height: 40,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 20,
}}></div>
<div>
<h1 style={{ margin: 0, fontSize: 24, fontWeight: 700, color: C.text }}>
SlideMap API Reference
</h1>
<p style={{ margin: 0, fontSize: 13, color: C.muted }}>
REST + SSE · CORS enabled · Any frontend can connect
</p>
</div>
</div>
{/* Base URL */}
<div style={{
background: C.surface, border: `1px solid ${C.border}`,
borderRadius: 8, padding: '10px 16px',
display: 'flex', alignItems: 'center', gap: 10, marginBottom: 24,
}}>
<span style={{ fontSize: 11, color: C.muted, fontWeight: 600, letterSpacing: 1 }}>BASE URL</span>
<code style={{ fontSize: 13, color: C.accent, flex: 1 }}>{BASE}</code>
<button
onClick={() => copy(BASE, 'base')}
style={{
background: 'transparent', border: `1px solid ${C.border}`,
borderRadius: 6, padding: '4px 10px', color: C.muted,
cursor: 'pointer', fontSize: 12,
}}
>{copied === 'base' ? '✓ Copied' : 'Copy'}</button>
</div>
{/* Flow diagram */}
<div style={{
background: C.surface, border: `1px solid ${C.border}`,
borderRadius: 8, padding: '14px 18px', marginBottom: 0,
fontSize: 13, color: C.muted, lineHeight: 1.7,
}}>
<strong style={{ color: C.text }}>📡 How it works</strong>
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
{['Teacher creates session', '→', 'Students look up & join', '→', 'Both subscribe to /api/events (SSE)', '→', 'Teacher uploads slides → students see live', '→', 'Teacher creates polls → students vote', '→', 'Students submit doubts → teacher resolves'].map((t, i) => (
t === '→'
? <span key={i} style={{ color: C.accent }}></span>
: <span key={i} style={{
background: '#1c2230', border: `1px solid ${C.border}`,
borderRadius: 5, padding: '2px 8px', color: C.text, fontSize: 12,
}}>{t}</span>
))}
</div>
</div>
{/* Tabs */}
<div style={{ display: 'flex', gap: 0, marginTop: 24, borderBottom: 'none' }}>
{TAGS.map(tag => (
<button
key={tag}
onClick={() => { setActiveTag(tag); setOpenIdx(null); }}
style={{
background: activeTag === tag ? C.accent : 'transparent',
color: activeTag === tag ? '#fff' : C.muted,
border: 'none', borderRadius: '8px 8px 0 0',
padding: '10px 18px', cursor: 'pointer', fontSize: 13,
fontWeight: activeTag === tag ? 600 : 400,
transition: 'all .15s',
}}
>{tag}</button>
))}
</div>
</div>
</div>
{/* ── Endpoints ── */}
<div style={{ maxWidth: 900, margin: '0 auto', padding: '24px 24px 60px' }}>
{filtered.map((ep, idx) => {
const isOpen = openIdx === idx;
return (
<div key={idx} style={{
background: C.surface,
border: `1px solid ${isOpen ? C.accent : C.border}`,
borderRadius: 10, marginBottom: 12,
transition: 'border-color .15s',
overflow: 'hidden',
}}>
{/* Header row */}
<button
onClick={() => setOpenIdx(isOpen ? null : idx)}
style={{
width: '100%', background: 'transparent', border: 'none',
padding: '14px 18px', cursor: 'pointer',
display: 'flex', alignItems: 'center', gap: 12, textAlign: 'left',
}}
>
<span style={{
background: METHOD_COLOR[ep.method] + '22',
color: METHOD_COLOR[ep.method],
border: `1px solid ${METHOD_COLOR[ep.method]}44`,
borderRadius: 5, padding: '2px 8px',
fontSize: 11, fontWeight: 700, letterSpacing: 1, minWidth: 42, textAlign: 'center',
}}>{ep.method}</span>
<code style={{ color: C.text, fontSize: 14, flex: 1, textAlign: 'left' }}>{ep.path}</code>
<span style={{ color: C.muted, fontSize: 14 }}>{ep.summary}</span>
<span style={{ color: C.muted, fontSize: 16, marginLeft: 4 }}>{isOpen ? '▲' : '▼'}</span>
</button>
{/* Body */}
{isOpen && (
<div style={{ padding: '0 18px 18px', borderTop: `1px solid ${C.border}` }}>
{/* Description */}
<p style={{ color: C.muted, fontSize: 14, marginTop: 14, whiteSpace: 'pre-line', lineHeight: 1.7 }}>
{ep.desc}
</p>
{/* Query params */}
{ep.params && (
<div style={{ marginTop: 16 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: C.purple, letterSpacing: 1, marginBottom: 8 }}>
QUERY PARAMETERS
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead>
<tr style={{ color: C.muted }}>
{['Name', 'Type', 'Required', 'Description'].map(h => (
<th key={h} style={{ textAlign: 'left', padding: '4px 8px', fontWeight: 500, borderBottom: `1px solid ${C.border}` }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{ep.params.map((p, pi) => (
<tr key={pi}>
<td style={{ padding: '6px 8px' }}><code style={{ color: C.orange }}>{p.name}</code></td>
<td style={{ padding: '6px 8px' }}><code style={{ color: C.purple }}>{p.type}</code></td>
<td style={{ padding: '6px 8px' }}><span style={{ color: p.required ? C.red : C.muted }}>{p.required ? 'Yes' : 'No'}</span></td>
<td style={{ padding: '6px 8px', color: C.muted }}>{p.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Request body */}
{ep.body && (
<div style={{ marginTop: 16 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: C.purple, letterSpacing: 1, marginBottom: 8 }}>
REQUEST BODY (JSON)
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead>
<tr style={{ color: C.muted }}>
{['Field', 'Type', 'Required', 'Description'].map(h => (
<th key={h} style={{ textAlign: 'left', padding: '4px 8px', fontWeight: 500, borderBottom: `1px solid ${C.border}` }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{ep.body.map((p, pi) => (
<tr key={pi} style={{ background: pi % 2 === 0 ? 'transparent' : '#ffffff05' }}>
<td style={{ padding: '6px 8px' }}><code style={{ color: C.orange }}>{p.name}</code></td>
<td style={{ padding: '6px 8px' }}><code style={{ color: C.purple }}>{p.type}</code></td>
<td style={{ padding: '6px 8px' }}><span style={{ color: p.required ? C.red : C.muted }}>{p.required ? 'Yes' : 'No'}</span></td>
<td style={{ padding: '6px 8px', color: C.muted }}>{p.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Example */}
{ep.example && (
<div style={{ marginTop: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: C.purple, letterSpacing: 1 }}>EXAMPLE</div>
<button
onClick={() => copy(ep.example!, `ex-${idx}`)}
style={{
background: 'transparent', border: `1px solid ${C.border}`,
borderRadius: 5, padding: '3px 10px', color: C.muted,
cursor: 'pointer', fontSize: 11,
}}
>{copied === `ex-${idx}` ? '✓ Copied' : 'Copy'}</button>
</div>
<pre style={{
background: '#0a0e14', border: `1px solid ${C.border}`,
borderRadius: 8, padding: 14, margin: 0,
fontSize: 12, color: C.text,
overflowX: 'auto', lineHeight: 1.6,
}}>{ep.example}</pre>
</div>
)}
{/* Response */}
{ep.response && (
<div style={{ marginTop: 12 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: C.green, letterSpacing: 1, marginBottom: 6 }}>RESPONSE</div>
<pre style={{
background: '#0a0e14', border: `1px solid ${C.border}`,
borderRadius: 8, padding: 14, margin: 0,
fontSize: 12, color: C.green,
overflowX: 'auto', lineHeight: 1.6,
}}>{ep.response}</pre>
</div>
)}
</div>
)}
</div>
);
})}
</div>
</div>
);
}