| |
| |
| |
| 'use client'; |
|
|
| import { useState } from 'react'; |
|
|
| const BASE = typeof window !== 'undefined' |
| ? `${window.location.protocol}//${window.location.host}` |
| : 'https://your-hf-space.hf.space'; |
|
|
| |
| 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', |
| }; |
|
|
| |
| 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[] = [ |
| |
| { |
| 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", ... } ] }`, |
| }, |
|
|
| |
| { |
| 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`, |
| }, |
|
|
| |
| { |
| 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 } }`, |
| }, |
|
|
| |
| { |
| 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 } }`, |
| }, |
|
|
| |
| { |
| 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 } } }`, |
| }, |
|
|
| |
| { |
| 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> |
| ); |
| } |
|
|