Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>GraphQL Playground</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
.cm-editor { | |
height: 100%; | |
font-size: 14px; | |
line-height: 1.5; | |
} | |
.cm-scroller { | |
font-family: 'Fira Code', monospace; | |
} | |
.cm-gutters { | |
background-color: #f8fafc ; | |
} | |
.json-viewer { | |
white-space: pre-wrap; | |
font-family: 'Fira Code', monospace; | |
font-size: 14px; | |
} | |
.graphiql-container { | |
--color-primary: #6366f1; | |
--color-secondary: #818cf8; | |
} | |
.tab-active { | |
border-bottom: 2px solid #6366f1; | |
color: #6366f1; | |
} | |
.query-history-item:hover { | |
background-color: #f1f5f9; | |
cursor: pointer; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50 min-h-screen"> | |
<div class="container mx-auto px-4 py-8"> | |
<!-- Header --> | |
<header class="mb-8"> | |
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4"> | |
<div> | |
<h1 class="text-3xl font-bold text-indigo-600 flex items-center gap-2"> | |
<i class="fas fa-project-diagram"></i> | |
GraphQL Explorer | |
</h1> | |
<p class="text-gray-600">Interactive GraphQL API playground</p> | |
</div> | |
<div class="flex gap-2"> | |
<button id="execute-btn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md flex items-center gap-2 transition-colors"> | |
<i class="fas fa-play"></i> Execute | |
</button> | |
<button id="save-btn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-md flex items-center gap-2 transition-colors"> | |
<i class="fas fa-save"></i> Save | |
</button> | |
<button id="history-btn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-md flex items-center gap-2 transition-colors"> | |
<i class="fas fa-history"></i> History | |
</button> | |
</div> | |
</div> | |
</header> | |
<!-- Main Content --> | |
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
<!-- Left Panel - Query Editor --> | |
<div class="lg:col-span-2 bg-white rounded-lg shadow-md overflow-hidden"> | |
<div class="border-b border-gray-200"> | |
<div class="flex"> | |
<button class="tab-btn px-4 py-3 font-medium text-gray-600 tab-active" data-tab="query"> | |
<i class="fas fa-code mr-2"></i>Query | |
</button> | |
<button class="tab-btn px-4 py-3 font-medium text-gray-600" data-tab="variables"> | |
<i class="fas fa-list mr-2"></i>Variables | |
</button> | |
<button class="tab-btn px-4 py-3 font-medium text-gray-600" data-tab="headers"> | |
<i class="fas fa-heading mr-2"></i>Headers | |
</button> | |
</div> | |
</div> | |
<div class="tab-content" id="query-tab"> | |
<div id="query-editor" class="h-96 p-4 font-mono">{ | |
characters(page: 2, filter: { name: "rick" }) { | |
info { | |
count | |
} | |
results { | |
name | |
status | |
species | |
type | |
gender | |
} | |
} | |
}</div> | |
</div> | |
<div class="tab-content hidden" id="variables-tab"> | |
<textarea id="variables-editor" class="w-full h-96 p-4 font-mono border-none focus:ring-0 resize-none" placeholder="Enter variables as JSON">{ | |
"page": 1, | |
"filter": { | |
"name": "rick" | |
} | |
}</textarea> | |
</div> | |
<div class="tab-content hidden" id="headers-tab"> | |
<textarea id="headers-editor" class="w-full h-96 p-4 font-mono border-none focus:ring-0 resize-none" placeholder="Enter headers as JSON">{ | |
"Authorization": "Bearer YOUR_TOKEN", | |
"Content-Type": "application/json" | |
}</textarea> | |
</div> | |
</div> | |
<!-- Right Panel - Results and Documentation --> | |
<div class="space-y-6"> | |
<!-- Results Panel --> | |
<div class="bg-white rounded-lg shadow-md overflow-hidden"> | |
<div class="border-b border-gray-200 px-4 py-3 font-medium text-gray-600 flex justify-between items-center"> | |
<span><i class="fas fa-chart-bar mr-2"></i>Results</span> | |
<div class="flex gap-2"> | |
<button id="prettify-btn" class="text-gray-500 hover:text-indigo-600"> | |
<i class="fas fa-align-left"></i> | |
</button> | |
<button id="copy-btn" class="text-gray-500 hover:text-indigo-600"> | |
<i class="fas fa-copy"></i> | |
</button> | |
</div> | |
</div> | |
<div id="results-container" class="p-4 h-64 overflow-auto json-viewer bg-gray-50"> | |
<p class="text-gray-400 italic">Results will appear here after execution</p> | |
</div> | |
</div> | |
<!-- Documentation Panel --> | |
<div class="bg-white rounded-lg shadow-md overflow-hidden"> | |
<div class="border-b border-gray-200 px-4 py-3 font-medium text-gray-600"> | |
<i class="fas fa-book mr-2"></i>Documentation | |
</div> | |
<div class="p-4 h-64 overflow-auto"> | |
<div class="space-y-4"> | |
<div> | |
<h3 class="font-medium text-indigo-600">Query</h3> | |
<ul class="ml-4 mt-2 space-y-1 text-sm text-gray-600"> | |
<li><span class="font-mono">characters</span> - Get all characters</li> | |
<li><span class="font-mono">character(id: ID!)</span> - Get a specific character</li> | |
<li><span class="font-mono">locations</span> - Get all locations</li> | |
<li><span class="font-mono">location(id: ID!)</span> - Get a specific location</li> | |
</ul> | |
</div> | |
<div> | |
<h3 class="font-medium text-indigo-600">Types</h3> | |
<ul class="ml-4 mt-2 space-y-1 text-sm text-gray-600"> | |
<li><span class="font-mono">Character</span> - A character in the series</li> | |
<li><span class="font-mono">Location</span> - A location in the series</li> | |
<li><span class="font-mono">Episode</span> - An episode in the series</li> | |
</ul> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Endpoint Configuration --> | |
<div class="mt-6 bg-white rounded-lg shadow-md p-4"> | |
<div class="flex flex-col md:flex-row gap-4 items-start md:items-center"> | |
<div class="flex-1"> | |
<label class="block text-sm font-medium text-gray-700 mb-1">GraphQL Endpoint</label> | |
<input type="text" id="endpoint-input" value="https://rickandmortyapi.com/graphql" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
</div> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Request Method</label> | |
<select id="method-select" class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
<option value="POST">POST</option> | |
<option value="GET">GET</option> | |
</select> | |
</div> | |
</div> | |
</div> | |
<!-- History Modal --> | |
<div id="history-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50"> | |
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] overflow-hidden"> | |
<div class="border-b border-gray-200 px-6 py-4 flex justify-between items-center"> | |
<h3 class="text-lg font-medium text-gray-900">Query History</h3> | |
<button id="close-history" class="text-gray-400 hover:text-gray-500"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="p-6 overflow-y-auto"> | |
<ul id="history-list" class="divide-y divide-gray-200"> | |
<!-- History items will be added here --> | |
</ul> | |
</div> | |
<div class="border-t border-gray-200 px-6 py-4 flex justify-end"> | |
<button id="clear-history" class="text-red-600 hover:text-red-800 mr-4">Clear History</button> | |
<button id="close-history-btn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md">Close</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Tab switching | |
const tabButtons = document.querySelectorAll('.tab-btn'); | |
const tabContents = document.querySelectorAll('.tab-content'); | |
tabButtons.forEach(button => { | |
button.addEventListener('click', () => { | |
// Remove active class from all buttons | |
tabButtons.forEach(btn => btn.classList.remove('tab-active')); | |
// Add active class to clicked button | |
button.classList.add('tab-active'); | |
// Hide all tab contents | |
tabContents.forEach(content => content.classList.add('hidden')); | |
// Show the selected tab content | |
const tabId = button.getAttribute('data-tab') + '-tab'; | |
document.getElementById(tabId).classList.remove('hidden'); | |
}); | |
}); | |
// Execute button | |
document.getElementById('execute-btn').addEventListener('click', executeQuery); | |
// History modal | |
const historyModal = document.getElementById('history-modal'); | |
document.getElementById('history-btn').addEventListener('click', () => { | |
historyModal.classList.remove('hidden'); | |
loadHistory(); | |
}); | |
document.getElementById('close-history').addEventListener('click', () => { | |
historyModal.classList.add('hidden'); | |
}); | |
document.getElementById('close-history-btn').addEventListener('click', () => { | |
historyModal.classList.add('hidden'); | |
}); | |
document.getElementById('clear-history').addEventListener('click', () => { | |
localStorage.removeItem('graphqlQueryHistory'); | |
document.getElementById('history-list').innerHTML = '<p class="text-gray-500 py-4">No queries in history</p>'; | |
}); | |
// Copy button | |
document.getElementById('copy-btn').addEventListener('click', () => { | |
const results = document.getElementById('results-container').textContent; | |
navigator.clipboard.writeText(results).then(() => { | |
const copyBtn = document.getElementById('copy-btn'); | |
copyBtn.innerHTML = '<i class="fas fa-check"></i>'; | |
setTimeout(() => { | |
copyBtn.innerHTML = '<i class="fas fa-copy"></i>'; | |
}, 2000); | |
}); | |
}); | |
// Prettify button | |
document.getElementById('prettify-btn').addEventListener('click', () => { | |
const resultsContainer = document.getElementById('results-container'); | |
try { | |
const json = JSON.parse(resultsContainer.textContent); | |
resultsContainer.textContent = JSON.stringify(json, null, 2); | |
} catch (e) { | |
// Not valid JSON, do nothing | |
} | |
}); | |
// Load CodeMirror for syntax highlighting | |
loadCodeMirror(); | |
function executeQuery() { | |
const query = document.getElementById('query-editor').textContent; | |
const variables = document.getElementById('variables-editor').value; | |
const headers = document.getElementById('headers-editor').value; | |
const endpoint = document.getElementById('endpoint-input').value; | |
const method = document.getElementById('method-select').value; | |
const resultsContainer = document.getElementById('results-container'); | |
resultsContainer.innerHTML = '<p class="text-gray-500 italic">Executing query...</p>'; | |
try { | |
const requestBody = { | |
query: query | |
}; | |
if (variables) { | |
requestBody.variables = JSON.parse(variables); | |
} | |
const requestHeaders = { | |
'Content-Type': 'application/json' | |
}; | |
if (headers) { | |
try { | |
const headersObj = JSON.parse(headers); | |
Object.assign(requestHeaders, headersObj); | |
} catch (e) { | |
console.error('Invalid headers JSON'); | |
} | |
} | |
const options = { | |
method: method, | |
headers: requestHeaders, | |
body: method === 'POST' ? JSON.stringify(requestBody) : null | |
}; | |
let url = endpoint; | |
if (method === 'GET') { | |
url += `?query=${encodeURIComponent(query)}`; | |
if (variables) { | |
url += `&variables=${encodeURIComponent(variables)}`; | |
} | |
} | |
fetch(url, options) | |
.then(response => response.json()) | |
.then(data => { | |
resultsContainer.textContent = JSON.stringify(data, null, 2); | |
saveToHistory(query, data); | |
}) | |
.catch(error => { | |
resultsContainer.textContent = `Error: ${error.message}`; | |
}); | |
} catch (error) { | |
resultsContainer.textContent = `Error: ${error.message}`; | |
} | |
} | |
function saveToHistory(query, response) { | |
const historyItem = { | |
query: query, | |
response: response, | |
timestamp: new Date().toISOString() | |
}; | |
let history = JSON.parse(localStorage.getItem('graphqlQueryHistory') || '[]'); | |
history.unshift(historyItem); | |
if (history.length > 10) { | |
history = history.slice(0, 10); | |
} | |
localStorage.setItem('graphqlQueryHistory', JSON.stringify(history)); | |
} | |
function loadHistory() { | |
const historyList = document.getElementById('history-list'); | |
const history = JSON.parse(localStorage.getItem('graphqlQueryHistory') || '[]'); | |
if (history.length === 0) { | |
historyList.innerHTML = '<p class="text-gray-500 py-4">No queries in history</p>'; | |
return; | |
} | |
historyList.innerHTML = ''; | |
history.forEach((item, index) => { | |
const li = document.createElement('li'); | |
li.className = 'py-4 query-history-item'; | |
li.innerHTML = ` | |
<div class="flex items-center justify-between"> | |
<div class="flex items-center"> | |
<span class="text-indigo-600 font-mono text-sm mr-3">#${index + 1}</span> | |
<div> | |
<p class="text-sm font-medium text-gray-900 truncate max-w-xs">${item.query.split('\n')[0].substring(0, 50)}...</p> | |
<p class="text-xs text-gray-500">${new Date(item.timestamp).toLocaleString()}</p> | |
</div> | |
</div> | |
<button class="text-indigo-600 hover:text-indigo-800" onclick="loadHistoryItem(${index})"> | |
<i class="fas fa-eye"></i> | |
</button> | |
</div> | |
`; | |
historyList.appendChild(li); | |
}); | |
} | |
window.loadHistoryItem = function(index) { | |
const history = JSON.parse(localStorage.getItem('graphqlQueryHistory') || '[]'); | |
if (index >= 0 && index < history.length) { | |
const item = history[index]; | |
document.getElementById('query-editor').textContent = item.query; | |
document.getElementById('results-container').textContent = JSON.stringify(item.response, null, 2); | |
historyModal.classList.add('hidden'); | |
} | |
}; | |
function loadCodeMirror() { | |
// In a real implementation, you would load CodeMirror here | |
// For this example, we'll just use textareas with monospace font | |
console.log('CodeMirror would be loaded here in a real implementation'); | |
} | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Fede-rueen/khan-graphlql" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |