diff --git a/README.md b/README.md index c46c989..b6752eb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,59 @@ -# REST-API-TESTER -A lightweight browser extension that allows you to test REST API endpoints directly from your browser. Toggle between JSON and FORM Data, view formatted responses with status codes and timing and save your frequently-used endpoints for quick access. Send custom headers. +# REST API Tester + +A lightweight Chrome extension that allows you to test REST API endpoints directly from your browser. + +## Features + +- **HTTP Methods**: Support for GET, POST, PUT, PATCH, and DELETE requests +- **Request Body**: Toggle between JSON and Form Data formats +- **Custom Headers**: Add and manage custom request headers +- **Response Viewer**: View formatted responses with syntax highlighting +- **Status Codes**: See HTTP status codes with color-coded badges +- **Response Timing**: Track response times in milliseconds +- **Save Endpoints**: Store frequently-used endpoints for quick access + +## Installation + +1. Clone this repository or download the source code +2. Open Chrome and navigate to `chrome://extensions/` +3. Enable "Developer mode" in the top right corner +4. Click "Load unpacked" and select the extension directory +5. The REST API Tester icon will appear in your browser toolbar + +## Usage + +1. Click the REST API Tester icon in your browser toolbar +2. Enter the URL of the API endpoint you want to test +3. Select the HTTP method (GET, POST, PUT, PATCH, DELETE) +4. Optionally add headers and request body +5. Click "Send" to make the request +6. View the response with status code and timing + +### Saving Endpoints + +1. After configuring your request, click "Save Current" +2. Enter a name for the endpoint +3. The endpoint will appear in the saved endpoints list +4. Click on a saved endpoint to load it + +## Tech Stack + +- **Manifest V3**: Chrome extension configuration +- **Vanilla JavaScript**: No frameworks required +- **Chrome Storage API**: For storing saved endpoints +- **Fetch API**: For making HTTP requests + +## Architecture + +``` +├── manifest.json # Extension configuration +├── background.js # Service worker for window management +├── popup.html # Main UI structure +├── popup.css # UI styling +├── popup.js # Request builder and response viewer logic +└── icons/ # Extension icons +``` + +## License + +MIT License diff --git a/background.js b/background.js new file mode 100644 index 0000000..13a65d2 --- /dev/null +++ b/background.js @@ -0,0 +1,38 @@ +// Background service worker for REST API Tester extension +// Handles window management for the extension + +let popupWindowId = null; + +// Listen for extension icon click to manage popup window +chrome.action.onClicked.addListener(async () => { + // Check if popup window already exists + if (popupWindowId !== null) { + try { + const window = await chrome.windows.get(popupWindowId); + // If window exists, focus it + await chrome.windows.update(popupWindowId, { focused: true }); + return; + } catch (e) { + // Window no longer exists, reset the ID + popupWindowId = null; + } + } + + // Create a new popup window + const window = await chrome.windows.create({ + url: chrome.runtime.getURL('popup.html'), + type: 'popup', + width: 800, + height: 700, + focused: true + }); + + popupWindowId = window.id; +}); + +// Track when the popup window is closed +chrome.windows.onRemoved.addListener((windowId) => { + if (windowId === popupWindowId) { + popupWindowId = null; + } +}); diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000..10cadbb Binary files /dev/null and b/icons/icon128.png differ diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000..7bc39f4 Binary files /dev/null and b/icons/icon16.png differ diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000..0b0466e Binary files /dev/null and b/icons/icon48.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..0ff37b6 --- /dev/null +++ b/manifest.json @@ -0,0 +1,29 @@ +{ + "manifest_version": 3, + "name": "REST API Tester", + "version": "1.0.0", + "description": "A lightweight browser extension that allows you to test REST API endpoints directly from your browser.", + "permissions": [ + "storage" + ], + "host_permissions": [ + "" + ], + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "default_title": "REST API Tester" + }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "background": { + "service_worker": "background.js" + } +} diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..9901bd1 --- /dev/null +++ b/popup.css @@ -0,0 +1,455 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background-color: #1a1a2e; + color: #eaeaea; + min-width: 600px; + min-height: 500px; +} + +.container { + padding: 20px; + max-width: 800px; + margin: 0 auto; +} + +header { + margin-bottom: 20px; + text-align: center; +} + +header h1 { + font-size: 1.5rem; + color: #00d4ff; +} + +/* Buttons */ +.btn { + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.2s, opacity 0.2s; +} + +.btn:hover { + opacity: 0.9; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background-color: #00d4ff; + color: #1a1a2e; + font-weight: 600; +} + +.btn-secondary { + background-color: #3a3a5a; + color: #eaeaea; +} + +.btn-add { + background-color: transparent; + color: #00d4ff; + border: 1px dashed #00d4ff; + margin-top: 8px; +} + +.btn-remove { + background-color: #ff4757; + color: white; + padding: 4px 8px; + font-size: 1rem; +} + +/* Saved Endpoints */ +.saved-endpoints { + margin-bottom: 20px; + padding: 15px; + background-color: #16213e; + border-radius: 8px; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.section-header h2 { + font-size: 1rem; + color: #00d4ff; +} + +.endpoints-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + max-height: 100px; + overflow-y: auto; +} + +.endpoint-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background-color: #3a3a5a; + border-radius: 20px; + font-size: 0.85rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.endpoint-item:hover { + background-color: #4a4a6a; +} + +.endpoint-item .method-badge { + font-size: 0.7rem; + padding: 2px 6px; + border-radius: 3px; + font-weight: 600; +} + +.endpoint-item .delete-endpoint { + background: none; + border: none; + color: #ff4757; + cursor: pointer; + padding: 0 4px; + font-size: 1rem; +} + +/* Method badge colors */ +.method-GET { background-color: #00c853; color: #1a1a2e; } +.method-POST { background-color: #2196f3; color: white; } +.method-PUT { background-color: #ff9800; color: #1a1a2e; } +.method-PATCH { background-color: #9c27b0; color: white; } +.method-DELETE { background-color: #f44336; color: white; } + +/* Request Builder */ +.request-builder { + margin-bottom: 20px; +} + +.request-line { + display: flex; + gap: 10px; + margin-bottom: 15px; +} + +.request-line select { + padding: 10px 15px; + border: none; + border-radius: 4px; + background-color: #16213e; + color: #eaeaea; + font-size: 0.9rem; + cursor: pointer; +} + +.request-line input { + flex: 1; + padding: 10px 15px; + border: 2px solid #3a3a5a; + border-radius: 4px; + background-color: #16213e; + color: #eaeaea; + font-size: 0.9rem; +} + +.request-line input:focus { + outline: none; + border-color: #00d4ff; +} + +/* Collapsible Sections */ +.collapsible-section { + margin-bottom: 10px; + border: 1px solid #3a3a5a; + border-radius: 4px; + overflow: hidden; +} + +.section-toggle { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + background-color: #16213e; + cursor: pointer; + user-select: none; +} + +.section-toggle:hover { + background-color: #1f2b4a; +} + +.toggle-icon { + transition: transform 0.2s; +} + +.toggle-icon.collapsed { + transform: rotate(-90deg); +} + +.section-content { + padding: 15px; + background-color: #0f0f23; +} + +.section-content.collapsed { + display: none; +} + +/* Headers */ +.header-row, .form-data-row { + display: flex; + gap: 10px; + margin-bottom: 8px; +} + +.header-row input, .form-data-row input { + flex: 1; + padding: 8px 12px; + border: 1px solid #3a3a5a; + border-radius: 4px; + background-color: #16213e; + color: #eaeaea; + font-size: 0.85rem; +} + +.header-row input:focus, .form-data-row input:focus { + outline: none; + border-color: #00d4ff; +} + +/* Body */ +.body-type-selector { + display: flex; + gap: 20px; + margin-bottom: 15px; +} + +.body-type-selector label { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; +} + +.body-type-selector input[type="radio"] { + accent-color: #00d4ff; +} + +#bodyInput { + width: 100%; + min-height: 120px; + padding: 12px; + border: 1px solid #3a3a5a; + border-radius: 4px; + background-color: #16213e; + color: #eaeaea; + font-family: 'Monaco', 'Menlo', 'Courier New', monospace; + font-size: 0.85rem; + resize: vertical; +} + +#bodyInput:focus { + outline: none; + border-color: #00d4ff; +} + +/* Response Section */ +.response-section { + padding: 15px; + background-color: #16213e; + border-radius: 8px; +} + +.response-header { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; +} + +.response-header h2 { + font-size: 1rem; + color: #00d4ff; +} + +.status-badge { + padding: 4px 12px; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 600; +} + +.status-badge.success { + background-color: #00c853; + color: #1a1a2e; +} + +.status-badge.redirect { + background-color: #ff9800; + color: #1a1a2e; +} + +.status-badge.client-error { + background-color: #ff4757; + color: white; +} + +.status-badge.server-error { + background-color: #9c27b0; + color: white; +} + +.response-time { + font-size: 0.85rem; + color: #888; +} + +.response-body { + background-color: #0f0f23; + border-radius: 4px; + padding: 15px; + overflow: auto; + max-height: 300px; +} + +.response-body pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +.response-body code { + font-family: 'Monaco', 'Menlo', 'Courier New', monospace; + font-size: 0.85rem; + color: #eaeaea; +} + +/* Loader */ +.loader { + display: flex; + align-items: center; + gap: 10px; + padding: 20px; + color: #888; +} + +.spinner { + width: 20px; + height: 20px; + border: 2px solid #3a3a5a; + border-top-color: #00d4ff; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Error Message */ +.error-message { + padding: 12px; + background-color: rgba(255, 71, 87, 0.2); + border: 1px solid #ff4757; + border-radius: 4px; + color: #ff4757; + margin-bottom: 15px; +} + +/* Modal */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background-color: #16213e; + padding: 25px; + border-radius: 8px; + min-width: 300px; +} + +.modal-content h3 { + margin-bottom: 15px; + color: #00d4ff; +} + +.modal-content input { + width: 100%; + padding: 10px 15px; + border: 1px solid #3a3a5a; + border-radius: 4px; + background-color: #0f0f23; + color: #eaeaea; + font-size: 0.9rem; + margin-bottom: 20px; +} + +.modal-content input:focus { + outline: none; + border-color: #00d4ff; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +/* Utility Classes */ +.hidden { + display: none !important; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #0f0f23; +} + +::-webkit-scrollbar-thumb { + background: #3a3a5a; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #4a4a6a; +} + +/* JSON Syntax Highlighting */ +.json-key { color: #ff79c6; } +.json-string { color: #f1fa8c; } +.json-number { color: #bd93f9; } +.json-boolean { color: #ff5555; } +.json-null { color: #6272a4; } diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..7c645f2 --- /dev/null +++ b/popup.html @@ -0,0 +1,123 @@ + + + + + + REST API Tester + + + +
+
+

REST API Tester

+
+ + +
+
+

Saved Endpoints

+ +
+
+
+ + +
+
+ + + +
+ + +
+
+ Headers + +
+
+
+
+ + + +
+
+ +
+
+ + +
+
+ Body + +
+
+
+ + + +
+ + +
+
+
+ + +
+
+

Response

+ + +
+ + +
+
Send a request to see the response here
+
+
+
+ + + + + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..54bdd27 --- /dev/null +++ b/popup.js @@ -0,0 +1,510 @@ +// REST API Tester - Main UI Logic + +// DOM Elements +const httpMethod = document.getElementById('httpMethod'); +const urlInput = document.getElementById('urlInput'); +const sendBtn = document.getElementById('sendBtn'); +const headersToggle = document.getElementById('headersToggle'); +const headersContent = document.getElementById('headersContent'); +const headersContainer = document.getElementById('headersContainer'); +const addHeaderBtn = document.getElementById('addHeaderBtn'); +const bodyToggle = document.getElementById('bodyToggle'); +const bodyContent = document.getElementById('bodyContent'); +const bodyTypeRadios = document.querySelectorAll('input[name="bodyType"]'); +const bodyInputContainer = document.getElementById('bodyInputContainer'); +const bodyInput = document.getElementById('bodyInput'); +const formDataContainer = document.getElementById('formDataContainer'); +const formDataFields = document.getElementById('formDataFields'); +const addFormFieldBtn = document.getElementById('addFormFieldBtn'); +const responseStatus = document.getElementById('responseStatus'); +const responseTime = document.getElementById('responseTime'); +const responseLoader = document.getElementById('responseLoader'); +const responseError = document.getElementById('responseError'); +const responseBody = document.getElementById('responseBody'); +const responseContent = document.getElementById('responseContent'); +const savedEndpointsList = document.getElementById('savedEndpointsList'); +const saveEndpointBtn = document.getElementById('saveEndpointBtn'); +const saveModal = document.getElementById('saveModal'); +const endpointNameInput = document.getElementById('endpointName'); +const cancelSaveBtn = document.getElementById('cancelSaveBtn'); +const confirmSaveBtn = document.getElementById('confirmSaveBtn'); + +// Initialize the application +document.addEventListener('DOMContentLoaded', () => { + loadSavedEndpoints(); + setupEventListeners(); +}); + +// Setup all event listeners +function setupEventListeners() { + // Toggle sections + headersToggle.addEventListener('click', () => toggleSection(headersToggle, headersContent)); + bodyToggle.addEventListener('click', () => toggleSection(bodyToggle, bodyContent)); + + // Add header row + addHeaderBtn.addEventListener('click', addHeaderRow); + + // Add form data field + addFormFieldBtn.addEventListener('click', addFormDataRow); + + // Body type change + bodyTypeRadios.forEach(radio => { + radio.addEventListener('change', handleBodyTypeChange); + }); + + // Send request + sendBtn.addEventListener('click', sendRequest); + + // Enter key on URL input + urlInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + sendRequest(); + } + }); + + // Save endpoint modal + saveEndpointBtn.addEventListener('click', openSaveModal); + cancelSaveBtn.addEventListener('click', closeSaveModal); + confirmSaveBtn.addEventListener('click', saveEndpoint); + + // Initial header row remove button + setupRemoveButtons(); +} + +// Toggle collapsible section +function toggleSection(toggle, content) { + const icon = toggle.querySelector('.toggle-icon'); + icon.classList.toggle('collapsed'); + content.classList.toggle('collapsed'); +} + +// Add new header row +function addHeaderRow() { + const row = document.createElement('div'); + row.className = 'header-row'; + row.innerHTML = ` + + + + `; + headersContainer.appendChild(row); + setupRemoveButtons(); +} + +// Add new form data row +function addFormDataRow() { + const row = document.createElement('div'); + row.className = 'form-data-row'; + row.innerHTML = ` + + + + `; + formDataFields.appendChild(row); + setupRemoveButtons(); +} + +// Setup remove buttons for headers and form data +function setupRemoveButtons() { + document.querySelectorAll('.btn-remove').forEach(btn => { + btn.onclick = function() { + const row = this.parentElement; + const container = row.parentElement; + // Keep at least one row + if (container.children.length > 1) { + row.remove(); + } else { + // Clear the inputs instead of removing + row.querySelectorAll('input').forEach(input => input.value = ''); + } + }; + }); +} + +// Handle body type change +function handleBodyTypeChange(e) { + const type = e.target.value; + bodyInputContainer.classList.add('hidden'); + formDataContainer.classList.add('hidden'); + + if (type === 'json') { + bodyInputContainer.classList.remove('hidden'); + bodyInput.placeholder = '{\n "key": "value"\n}'; + } else if (type === 'form') { + formDataContainer.classList.remove('hidden'); + } +} + +// Get headers from the form +function getHeaders() { + const headers = {}; + document.querySelectorAll('.header-row').forEach(row => { + const key = row.querySelector('.header-key').value.trim(); + const value = row.querySelector('.header-value').value.trim(); + if (key && value) { + headers[key] = value; + } + }); + return headers; +} + +// Get body content based on type +function getBody() { + const bodyType = document.querySelector('input[name="bodyType"]:checked').value; + + if (bodyType === 'none') { + return null; + } + + if (bodyType === 'json') { + const jsonBody = bodyInput.value.trim(); + return jsonBody || null; + } + + if (bodyType === 'form') { + const formData = new URLSearchParams(); + document.querySelectorAll('.form-data-row').forEach(row => { + const key = row.querySelector('.form-key').value.trim(); + const value = row.querySelector('.form-value').value.trim(); + if (key) { + formData.append(key, value); + } + }); + return formData.toString() || null; + } + + return null; +} + +// Get content type based on body type +function getContentType() { + const bodyType = document.querySelector('input[name="bodyType"]:checked').value; + if (bodyType === 'json') { + return 'application/json'; + } + if (bodyType === 'form') { + return 'application/x-www-form-urlencoded'; + } + return null; +} + +// Send the HTTP request +async function sendRequest() { + const url = urlInput.value.trim(); + + if (!url) { + showError('Please enter a URL'); + return; + } + + // Validate URL + try { + new URL(url); + } catch (e) { + showError('Please enter a valid URL'); + return; + } + + // Show loader + responseLoader.classList.remove('hidden'); + responseError.classList.add('hidden'); + responseStatus.classList.add('hidden'); + responseTime.classList.add('hidden'); + responseContent.textContent = ''; + sendBtn.disabled = true; + + const method = httpMethod.value; + const headers = getHeaders(); + const body = getBody(); + const contentType = getContentType(); + + if (contentType) { + headers['Content-Type'] = contentType; + } + + const options = { + method, + headers + }; + + // Only add body for methods that support it + if (['POST', 'PUT', 'PATCH'].includes(method) && body) { + options.body = body; + } + + const startTime = performance.now(); + + try { + const response = await fetch(url, options); + const endTime = performance.now(); + const duration = Math.round(endTime - startTime); + + // Hide loader + responseLoader.classList.add('hidden'); + sendBtn.disabled = false; + + // Show status + showStatus(response.status, response.statusText); + showResponseTime(duration); + + // Get response body + const contentType = response.headers.get('content-type'); + let responseText; + let isFormattedJson = false; + + if (contentType && contentType.includes('application/json')) { + try { + const json = await response.json(); + responseText = formatJSON(json); + isFormattedJson = true; + } catch (parseError) { + // If JSON parsing fails, treat as plain text + responseText = await response.text(); + } + } else { + responseText = await response.text(); + } + + // Use innerHTML only for our formatted JSON (which is safely escaped in formatJSON) + // Use textContent for plain text to prevent XSS + if (isFormattedJson) { + responseContent.innerHTML = responseText; + } else { + responseContent.textContent = responseText; + } + responseBody.classList.remove('hidden'); + + } catch (error) { + // Hide loader + responseLoader.classList.add('hidden'); + sendBtn.disabled = false; + + showError(`Request failed: ${error.message}`); + } +} + +// Show status badge +function showStatus(status, statusText) { + responseStatus.textContent = `${status} ${statusText}`; + responseStatus.classList.remove('hidden', 'success', 'redirect', 'client-error', 'server-error'); + + if (status >= 200 && status < 300) { + responseStatus.classList.add('success'); + } else if (status >= 300 && status < 400) { + responseStatus.classList.add('redirect'); + } else if (status >= 400 && status < 500) { + responseStatus.classList.add('client-error'); + } else if (status >= 500) { + responseStatus.classList.add('server-error'); + } +} + +// Show response time +function showResponseTime(duration) { + responseTime.textContent = `${duration}ms`; + responseTime.classList.remove('hidden'); +} + +// Show error message +function showError(message) { + responseError.textContent = message; + responseError.classList.remove('hidden'); + responseLoader.classList.add('hidden'); +} + +// Format JSON with syntax highlighting +function formatJSON(json) { + const jsonString = JSON.stringify(json, null, 2); + return jsonString + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?)/g, (match) => { + let cls = 'json-string'; + if (/:$/.test(match)) { + cls = 'json-key'; + } + return `${match}`; + }) + .replace(/\b(true|false)\b/g, '$1') + .replace(/\bnull\b/g, 'null') + .replace(/\b(-?\d+\.?\d*)\b/g, '$1'); +} + +// Load saved endpoints from Chrome storage +function loadSavedEndpoints() { + if (typeof chrome !== 'undefined' && chrome.storage) { + chrome.storage.local.get(['savedEndpoints'], (result) => { + const endpoints = result.savedEndpoints || []; + renderSavedEndpoints(endpoints); + }); + } +} + +// Render saved endpoints list +function renderSavedEndpoints(endpoints) { + savedEndpointsList.innerHTML = ''; + + if (endpoints.length === 0) { + savedEndpointsList.innerHTML = 'No saved endpoints'; + return; + } + + endpoints.forEach((endpoint, index) => { + const item = document.createElement('div'); + item.className = 'endpoint-item'; + item.innerHTML = ` + ${endpoint.method} + ${endpoint.name} + + `; + + // Load endpoint on click (except delete button) + item.addEventListener('click', (e) => { + if (!e.target.classList.contains('delete-endpoint')) { + loadEndpoint(endpoint); + } + }); + + // Delete endpoint + item.querySelector('.delete-endpoint').addEventListener('click', (e) => { + e.stopPropagation(); + deleteEndpoint(index); + }); + + savedEndpointsList.appendChild(item); + }); +} + +// Load endpoint into the form +function loadEndpoint(endpoint) { + httpMethod.value = endpoint.method; + urlInput.value = endpoint.url; + + // Load headers + headersContainer.innerHTML = ''; + if (endpoint.headers && Object.keys(endpoint.headers).length > 0) { + Object.entries(endpoint.headers).forEach(([key, value]) => { + const row = document.createElement('div'); + row.className = 'header-row'; + row.innerHTML = ` + + + + `; + headersContainer.appendChild(row); + }); + } else { + addHeaderRow(); + } + + // Load body type and content + const bodyType = endpoint.bodyType || 'none'; + document.querySelector(`input[name="bodyType"][value="${bodyType}"]`).checked = true; + + bodyInputContainer.classList.add('hidden'); + formDataContainer.classList.add('hidden'); + + if (bodyType === 'json') { + bodyInputContainer.classList.remove('hidden'); + bodyInput.value = endpoint.body || ''; + } else if (bodyType === 'form') { + formDataContainer.classList.remove('hidden'); + formDataFields.innerHTML = ''; + if (endpoint.formData && endpoint.formData.length > 0) { + endpoint.formData.forEach(({ key, value }) => { + const row = document.createElement('div'); + row.className = 'form-data-row'; + row.innerHTML = ` + + + + `; + formDataFields.appendChild(row); + }); + } else { + addFormDataRow(); + } + } + + setupRemoveButtons(); +} + +// Open save modal +function openSaveModal() { + const url = urlInput.value.trim(); + if (!url) { + showError('Please enter a URL first'); + return; + } + endpointNameInput.value = ''; + saveModal.classList.remove('hidden'); + endpointNameInput.focus(); +} + +// Close save modal +function closeSaveModal() { + saveModal.classList.add('hidden'); +} + +// Save endpoint to Chrome storage +function saveEndpoint() { + const name = endpointNameInput.value.trim(); + if (!name) { + return; + } + + const bodyType = document.querySelector('input[name="bodyType"]:checked').value; + const formData = []; + + if (bodyType === 'form') { + document.querySelectorAll('.form-data-row').forEach(row => { + const key = row.querySelector('.form-key').value.trim(); + const value = row.querySelector('.form-value').value.trim(); + if (key) { + formData.push({ key, value }); + } + }); + } + + const endpoint = { + name, + method: httpMethod.value, + url: urlInput.value.trim(), + headers: getHeaders(), + bodyType, + body: bodyType === 'json' ? bodyInput.value : '', + formData + }; + + if (typeof chrome !== 'undefined' && chrome.storage) { + chrome.storage.local.get(['savedEndpoints'], (result) => { + const endpoints = result.savedEndpoints || []; + endpoints.push(endpoint); + chrome.storage.local.set({ savedEndpoints: endpoints }, () => { + renderSavedEndpoints(endpoints); + closeSaveModal(); + }); + }); + } +} + +// Delete endpoint from Chrome storage +function deleteEndpoint(index) { + if (typeof chrome !== 'undefined' && chrome.storage) { + chrome.storage.local.get(['savedEndpoints'], (result) => { + const endpoints = result.savedEndpoints || []; + endpoints.splice(index, 1); + chrome.storage.local.set({ savedEndpoints: endpoints }, () => { + renderSavedEndpoints(endpoints); + }); + }); + } +} + +// Escape HTML to prevent XSS +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +}