diff --git a/src/plays/api-request-builder/ApiRequestBuilder.jsx b/src/plays/api-request-builder/ApiRequestBuilder.jsx new file mode 100644 index 000000000..3f939357b --- /dev/null +++ b/src/plays/api-request-builder/ApiRequestBuilder.jsx @@ -0,0 +1,218 @@ +import PlayHeader from 'common/playlists/PlayHeader'; +import { useState, useEffect } from 'react'; +import RequestPanel from './components/RequestPanel'; +import ResponsePanel from './components/ResponsePanel'; +import HistoryPanel from './components/HistoryPanel'; +import './styles.css'; + +function ApiRequestBuilder(props) { + const [method, setMethod] = useState('GET'); + const [url, setUrl] = useState('https://jsonplaceholder.typicode.com/posts/1'); + const [headers, setHeaders] = useState([ + { key: 'Content-Type', value: 'application/json', enabled: true } + ]); + const [body, setBody] = useState(''); + const [response, setResponse] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [history, setHistory] = useState([]); + const [activeTab, setActiveTab] = useState('body'); + const [bodyType, setBodyType] = useState('json'); + const [showHistory, setShowHistory] = useState(false); + + // Load history from localStorage on mount + useEffect(() => { + const savedHistory = localStorage.getItem('api-request-history'); + if (savedHistory) { + try { + setHistory(JSON.parse(savedHistory)); + } catch (error) { + console.error('Failed to load history:', error); + } + } + }, []); + + // Save history to localStorage + const saveToHistory = (request, response) => { + const historyItem = { + id: Date.now(), + timestamp: new Date().toISOString(), + method: request.method, + url: request.url, + headers: request.headers, + body: request.body, + response: { + status: response.status, + statusText: response.statusText, + data: response.data, + time: response.time, + size: response.size + } + }; + + const updatedHistory = [historyItem, ...history].slice(0, 50); // Keep last 50 requests + setHistory(updatedHistory); + localStorage.setItem('api-request-history', JSON.stringify(updatedHistory)); + }; + + const handleSendRequest = async () => { + if (!url.trim()) { + setResponse({ + error: true, + message: 'Please enter a valid URL', + status: 0 + }); + + return; + } + + setIsLoading(true); + const startTime = Date.now(); + + try { + // Prepare headers + const requestHeaders = {}; + headers.forEach((header) => { + if (header.enabled && header.key.trim()) { + requestHeaders[header.key] = header.value; + } + }); + + // Prepare request options + const options = { + method: method, + headers: requestHeaders + }; + + // Add body for methods that support it + if (['POST', 'PUT', 'PATCH'].includes(method) && body.trim()) { + if (bodyType === 'json') { + try { + JSON.parse(body); // Validate JSON + options.body = body; + } catch (e) { + throw new Error('Invalid JSON in request body'); + } + } else { + options.body = body; + } + } + + const response = await fetch(url, options); + const endTime = Date.now(); + const responseTime = endTime - startTime; + + let responseData; + const contentType = response.headers.get('content-type'); + + if (contentType && contentType.includes('application/json')) { + responseData = await response.json(); + } else { + responseData = await response.text(); + } + + const responseSize = new Blob([JSON.stringify(responseData)]).size; + + const responseObj = { + status: response.status, + statusText: response.statusText, + data: responseData, + headers: Object.fromEntries(response.headers.entries()), + time: responseTime, + size: responseSize, + error: false + }; + + setResponse(responseObj); + + // Save to history + saveToHistory({ method, url, headers, body }, responseObj); + } catch (error) { + setResponse({ + error: true, + message: error.message, + status: 0, + time: Date.now() - startTime + }); + } finally { + setIsLoading(false); + } + }; + + const loadFromHistory = (item) => { + setMethod(item.method); + setUrl(item.url); + setHeaders(item.headers); + setBody(item.body || ''); + setResponse(item.response); + setShowHistory(false); + }; + + const clearHistory = () => { + setHistory([]); + localStorage.removeItem('api-request-history'); + }; + + const formatJSON = () => { + try { + const parsed = JSON.parse(body); + setBody(JSON.stringify(parsed, null, 2)); + } catch (error) { + alert('Invalid JSON format'); + } + }; + + return ( +
+ +
+
+
+

🚀 API Request Builder & Tester

+ +
+ +
+
+ + + +
+ + {showHistory && ( + setShowHistory(false)} + onLoadRequest={loadFromHistory} + /> + )} +
+
+
+
+ ); +} + +export default ApiRequestBuilder; diff --git a/src/plays/api-request-builder/components/HistoryPanel.jsx b/src/plays/api-request-builder/components/HistoryPanel.jsx new file mode 100644 index 000000000..43d26560b --- /dev/null +++ b/src/plays/api-request-builder/components/HistoryPanel.jsx @@ -0,0 +1,114 @@ +function HistoryPanel({ history, onLoadRequest, onClearHistory, onClose }) { + const formatDate = (isoString) => { + const date = new Date(isoString); + const now = new Date(); + const diffMs = now - date; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + + return date.toLocaleDateString(); + }; + + const getStatusEmoji = (status) => { + if (status >= 200 && status < 300) return '✅'; + if (status >= 300 && status < 400) return 'â†Šī¸'; + if (status >= 400 && status < 500) return 'âš ī¸'; + + return '❌'; + }; + + const getMethodColor = (method) => { + const colors = { + GET: '#28a745', + POST: '#ffc107', + PUT: '#17a2b8', + PATCH: '#6f42c1', + DELETE: '#dc3545', + HEAD: '#6c757d', + OPTIONS: '#343a40' + }; + + return colors[method] || '#6c757d'; + }; + + return ( +
+
+

📋 Request History

+
+ {history.length > 0 && ( + + )} + +
+
+ +
+ {history.length === 0 ? ( +
+
📭
+

No requests yet

+ Your request history will appear here +
+ ) : ( +
+ {history.map((item) => ( +
onLoadRequest(item)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + onLoadRequest(item); + } + }} + > +
+ + {item.method} + + + {getStatusEmoji(item.response.status)} {item.response.status} + + {formatDate(item.timestamp)} +
+
+ {item.url} +
+
+ ⚡ {item.response.time}ms + {item.response.size && đŸ“Ļ {Math.round(item.response.size / 1024)}KB} +
+
+ ))} +
+ )} +
+ +
+ 💡 Click on any request to load it +
+
+ ); +} + +export default HistoryPanel; diff --git a/src/plays/api-request-builder/components/RequestPanel.jsx b/src/plays/api-request-builder/components/RequestPanel.jsx new file mode 100644 index 000000000..95e65e8ff --- /dev/null +++ b/src/plays/api-request-builder/components/RequestPanel.jsx @@ -0,0 +1,188 @@ +const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; + +function RequestPanel({ + method, + setMethod, + url, + setUrl, + headers, + setHeaders, + body, + setBody, + activeTab, + setActiveTab, + bodyType, + setBodyType, + onSend, + isLoading, + formatJSON +}) { + const addHeader = () => { + setHeaders([...headers, { key: '', value: '', enabled: true }]); + }; + + const updateHeader = (index, field, value) => { + const newHeaders = [...headers]; + newHeaders[index][field] = value; + setHeaders(newHeaders); + }; + + const removeHeader = (index) => { + setHeaders(headers.filter((_, i) => i !== index)); + }; + + const toggleHeader = (index) => { + const newHeaders = [...headers]; + newHeaders[index].enabled = !newHeaders[index].enabled; + setHeaders(newHeaders); + }; + + const handleKeyDown = (e) => { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + onSend(); + } + }; + + return ( +
+
+ + + setUrl(e.target.value)} + onKeyDown={handleKeyDown} + /> + + +
+ +
+ + +
+ +
+ {activeTab === 'headers' && ( +
+
+ Headers + +
+
+ {headers.map((header, index) => ( +
+ toggleHeader(index)} + /> + updateHeader(index, 'key', e.target.value)} + /> + updateHeader(index, 'value', e.target.value)} + /> + +
+ ))} +
+
+ )} + + {activeTab === 'body' && ( +
+
+
+ + +
+ {bodyType === 'json' && ( + + )} +
+