Skip to content

Commit 05f435f

Browse files
authored
This PR introduces a React-based API Testing Tool that allows users to build, send, and test HTTP requests directly within ReactPlay. (#1651)
* Added api tester * Added a play * made prettier changes * Added a play * Abc * fix: Remove hardcoded API keys and secure environment variables - Remove tracked .env.development from git - Create .env.example template without secrets - Update .gitignore to exclude .env.development - Update README with secure setup instructions - Fixes Netlify secrets scanning deployment error
1 parent 324b3fe commit 05f435f

10 files changed

Lines changed: 1616 additions & 3 deletions

File tree

.env.development renamed to .env.example

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,12 @@ DISABLE_ESLINT_PLUGIN=true
1010
REACT_APP_ACTIVITIES_ON=true
1111
REACT_APP_ACTIVITY_ID=hackrplay
1212
REACT_APP_DADJOKES_URL=https://jokeapi-v2.p.rapidapi.com/joke/
13-
REACT_APP_DADJOKES_APIKEY='b71df95c75msha446fab91d0e935p1d0262jsn1d938cb85502'
14-
REACT_APP_DADJOKES_APIHOST='jokeapi-v2.p.rapidapi.com'
13+
REACT_APP_DADJOKES_APIKEY=your_rapidapi_key_here
14+
REACT_APP_DADJOKES_APIHOST='jokeapi-v2.p.rapidapi.com'
15+
16+
# Add your API keys below:
17+
# REACT_APP_SEARCH_APIKEY=your_search_api_key
18+
# REACT_APP_TUBETUNES_APIKEY=your_tubetunes_api_key
19+
# REACT_APP_WEATHER_API_KEY=your_weather_api_key
20+
# REACT_APP_FIREBASE_API_KEY=your_firebase_api_key
21+
# REACT_APP_DIGITSDELIGHT_APIKEY=your_digitsdelight_api_key

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
# misc
1717
.env
18+
.env.development
1819
.DS_Store
1920
.env
2021
.env.local

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ Once forked, you can clone the repo by clicking the `Clone or Download` button o
8888

8989
Please change the directory after cloning the repository using the `cd <folder-name>` command.
9090

91-
> **Note:** Please do not remove the `.env.development` file from the root folder. It contains all the environment variables required for development.
91+
> **Note:** Copy `.env.example` to `.env.development` and add your API keys. The `.env.development` file is git-ignored to keep secrets secure.
9292
9393
### ⬇️ Install Dependencies
9494

netlify.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
[build]
2+
command = "npm run build"
3+
publish = "build"
4+
5+
[[headers]]
6+
for = "/*"
7+
[headers.values]
8+
X-Frame-Options = "DENY"
9+
110
[[redirects]]
211
from = "/*"
312
to = "/index.html"
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import PlayHeader from 'common/playlists/PlayHeader';
2+
import { useState, useEffect } from 'react';
3+
import RequestPanel from './components/RequestPanel';
4+
import ResponsePanel from './components/ResponsePanel';
5+
import HistoryPanel from './components/HistoryPanel';
6+
import './styles.css';
7+
8+
function ApiRequestBuilder(props) {
9+
const [method, setMethod] = useState('GET');
10+
const [url, setUrl] = useState('https://jsonplaceholder.typicode.com/posts/1');
11+
const [headers, setHeaders] = useState([
12+
{ key: 'Content-Type', value: 'application/json', enabled: true }
13+
]);
14+
const [body, setBody] = useState('');
15+
const [response, setResponse] = useState(null);
16+
const [isLoading, setIsLoading] = useState(false);
17+
const [history, setHistory] = useState([]);
18+
const [activeTab, setActiveTab] = useState('body');
19+
const [bodyType, setBodyType] = useState('json');
20+
const [showHistory, setShowHistory] = useState(false);
21+
22+
// Load history from localStorage on mount
23+
useEffect(() => {
24+
const savedHistory = localStorage.getItem('api-request-history');
25+
if (savedHistory) {
26+
try {
27+
setHistory(JSON.parse(savedHistory));
28+
} catch (error) {
29+
console.error('Failed to load history:', error);
30+
}
31+
}
32+
}, []);
33+
34+
// Save history to localStorage
35+
const saveToHistory = (request, response) => {
36+
const historyItem = {
37+
id: Date.now(),
38+
timestamp: new Date().toISOString(),
39+
method: request.method,
40+
url: request.url,
41+
headers: request.headers,
42+
body: request.body,
43+
response: {
44+
status: response.status,
45+
statusText: response.statusText,
46+
data: response.data,
47+
time: response.time,
48+
size: response.size
49+
}
50+
};
51+
52+
const updatedHistory = [historyItem, ...history].slice(0, 50); // Keep last 50 requests
53+
setHistory(updatedHistory);
54+
localStorage.setItem('api-request-history', JSON.stringify(updatedHistory));
55+
};
56+
57+
const handleSendRequest = async () => {
58+
if (!url.trim()) {
59+
setResponse({
60+
error: true,
61+
message: 'Please enter a valid URL',
62+
status: 0
63+
});
64+
65+
return;
66+
}
67+
68+
setIsLoading(true);
69+
const startTime = Date.now();
70+
71+
try {
72+
// Prepare headers
73+
const requestHeaders = {};
74+
headers.forEach((header) => {
75+
if (header.enabled && header.key.trim()) {
76+
requestHeaders[header.key] = header.value;
77+
}
78+
});
79+
80+
// Prepare request options
81+
const options = {
82+
method: method,
83+
headers: requestHeaders
84+
};
85+
86+
// Add body for methods that support it
87+
if (['POST', 'PUT', 'PATCH'].includes(method) && body.trim()) {
88+
if (bodyType === 'json') {
89+
try {
90+
JSON.parse(body); // Validate JSON
91+
options.body = body;
92+
} catch (e) {
93+
throw new Error('Invalid JSON in request body');
94+
}
95+
} else {
96+
options.body = body;
97+
}
98+
}
99+
100+
const response = await fetch(url, options);
101+
const endTime = Date.now();
102+
const responseTime = endTime - startTime;
103+
104+
let responseData;
105+
const contentType = response.headers.get('content-type');
106+
107+
if (contentType && contentType.includes('application/json')) {
108+
responseData = await response.json();
109+
} else {
110+
responseData = await response.text();
111+
}
112+
113+
const responseSize = new Blob([JSON.stringify(responseData)]).size;
114+
115+
const responseObj = {
116+
status: response.status,
117+
statusText: response.statusText,
118+
data: responseData,
119+
headers: Object.fromEntries(response.headers.entries()),
120+
time: responseTime,
121+
size: responseSize,
122+
error: false
123+
};
124+
125+
setResponse(responseObj);
126+
127+
// Save to history
128+
saveToHistory({ method, url, headers, body }, responseObj);
129+
} catch (error) {
130+
setResponse({
131+
error: true,
132+
message: error.message,
133+
status: 0,
134+
time: Date.now() - startTime
135+
});
136+
} finally {
137+
setIsLoading(false);
138+
}
139+
};
140+
141+
const loadFromHistory = (item) => {
142+
setMethod(item.method);
143+
setUrl(item.url);
144+
setHeaders(item.headers);
145+
setBody(item.body || '');
146+
setResponse(item.response);
147+
setShowHistory(false);
148+
};
149+
150+
const clearHistory = () => {
151+
setHistory([]);
152+
localStorage.removeItem('api-request-history');
153+
};
154+
155+
const formatJSON = () => {
156+
try {
157+
const parsed = JSON.parse(body);
158+
setBody(JSON.stringify(parsed, null, 2));
159+
} catch (error) {
160+
alert('Invalid JSON format');
161+
}
162+
};
163+
164+
return (
165+
<div className="play-details">
166+
<PlayHeader play={props} />
167+
<div className="play-details-body">
168+
<div className="api-builder-container">
169+
<div className="api-builder-header">
170+
<h2 className="api-builder-title">🚀 API Request Builder & Tester</h2>
171+
<button
172+
className="history-toggle-btn"
173+
title="View Request History"
174+
onClick={() => setShowHistory(!showHistory)}
175+
>
176+
📋 History ({history.length})
177+
</button>
178+
</div>
179+
180+
<div className={`api-builder-layout ${showHistory ? 'show-history' : ''}`}>
181+
<div className="api-builder-main">
182+
<RequestPanel
183+
activeTab={activeTab}
184+
body={body}
185+
bodyType={bodyType}
186+
formatJSON={formatJSON}
187+
headers={headers}
188+
isLoading={isLoading}
189+
method={method}
190+
setActiveTab={setActiveTab}
191+
setBody={setBody}
192+
setBodyType={setBodyType}
193+
setHeaders={setHeaders}
194+
setMethod={setMethod}
195+
setUrl={setUrl}
196+
url={url}
197+
onSend={handleSendRequest}
198+
/>
199+
200+
<ResponsePanel isLoading={isLoading} response={response} />
201+
</div>
202+
203+
{showHistory && (
204+
<HistoryPanel
205+
history={history}
206+
onClearHistory={clearHistory}
207+
onClose={() => setShowHistory(false)}
208+
onLoadRequest={loadFromHistory}
209+
/>
210+
)}
211+
</div>
212+
</div>
213+
</div>
214+
</div>
215+
);
216+
}
217+
218+
export default ApiRequestBuilder;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
function HistoryPanel({ history, onLoadRequest, onClearHistory, onClose }) {
2+
const formatDate = (isoString) => {
3+
const date = new Date(isoString);
4+
const now = new Date();
5+
const diffMs = now - date;
6+
const diffMins = Math.floor(diffMs / 60000);
7+
const diffHours = Math.floor(diffMs / 3600000);
8+
const diffDays = Math.floor(diffMs / 86400000);
9+
10+
if (diffMins < 1) return 'Just now';
11+
if (diffMins < 60) return `${diffMins}m ago`;
12+
if (diffHours < 24) return `${diffHours}h ago`;
13+
if (diffDays < 7) return `${diffDays}d ago`;
14+
15+
return date.toLocaleDateString();
16+
};
17+
18+
const getStatusEmoji = (status) => {
19+
if (status >= 200 && status < 300) return '✅';
20+
if (status >= 300 && status < 400) return '↩️';
21+
if (status >= 400 && status < 500) return '⚠️';
22+
23+
return '❌';
24+
};
25+
26+
const getMethodColor = (method) => {
27+
const colors = {
28+
GET: '#28a745',
29+
POST: '#ffc107',
30+
PUT: '#17a2b8',
31+
PATCH: '#6f42c1',
32+
DELETE: '#dc3545',
33+
HEAD: '#6c757d',
34+
OPTIONS: '#343a40'
35+
};
36+
37+
return colors[method] || '#6c757d';
38+
};
39+
40+
return (
41+
<div className="history-panel">
42+
<div className="history-header">
43+
<h3>📋 Request History</h3>
44+
<div className="history-actions">
45+
{history.length > 0 && (
46+
<button
47+
className="clear-history-btn"
48+
title="Clear all history"
49+
onClick={onClearHistory}
50+
>
51+
🗑️ Clear
52+
</button>
53+
)}
54+
<button className="close-history-btn" title="Close history" onClick={onClose}>
55+
56+
</button>
57+
</div>
58+
</div>
59+
60+
<div className="history-content">
61+
{history.length === 0 ? (
62+
<div className="history-empty">
63+
<div className="empty-icon">📭</div>
64+
<p>No requests yet</p>
65+
<small>Your request history will appear here</small>
66+
</div>
67+
) : (
68+
<div className="history-list">
69+
{history.map((item) => (
70+
<div
71+
className="history-item"
72+
key={item.id}
73+
role="button"
74+
tabIndex={0}
75+
onClick={() => onLoadRequest(item)}
76+
onKeyDown={(e) => {
77+
if (e.key === 'Enter' || e.key === ' ') {
78+
onLoadRequest(item);
79+
}
80+
}}
81+
>
82+
<div className="history-item-header">
83+
<span
84+
className="history-method"
85+
style={{ backgroundColor: getMethodColor(item.method) }}
86+
>
87+
{item.method}
88+
</span>
89+
<span className="history-status">
90+
{getStatusEmoji(item.response.status)} {item.response.status}
91+
</span>
92+
<span className="history-time">{formatDate(item.timestamp)}</span>
93+
</div>
94+
<div className="history-item-url" title={item.url}>
95+
{item.url}
96+
</div>
97+
<div className="history-item-footer">
98+
<span>{item.response.time}ms</span>
99+
{item.response.size && <span>📦 {Math.round(item.response.size / 1024)}KB</span>}
100+
</div>
101+
</div>
102+
))}
103+
</div>
104+
)}
105+
</div>
106+
107+
<div className="history-footer">
108+
<small>💡 Click on any request to load it</small>
109+
</div>
110+
</div>
111+
);
112+
}
113+
114+
export default HistoryPanel;

0 commit comments

Comments
 (0)