← back to Watches
public/api-playground.html
701 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Omega Watch API - Interactive Playground</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 36px;
margin-bottom: 10px;
}
.header p {
font-size: 18px;
opacity: 0.9;
}
.content {
display: grid;
grid-template-columns: 400px 1fr;
min-height: 600px;
}
.sidebar {
background: #f8f9fa;
border-right: 1px solid #dee2e6;
padding: 20px;
overflow-y: auto;
max-height: calc(100vh - 200px);
}
.endpoint-group {
margin-bottom: 30px;
}
.endpoint-group h3 {
font-size: 14px;
text-transform: uppercase;
color: #6c757d;
margin-bottom: 10px;
font-weight: 600;
}
.endpoint {
background: white;
padding: 12px;
margin-bottom: 8px;
border-radius: 8px;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
}
.endpoint:hover {
border-color: #667eea;
transform: translateX(4px);
}
.endpoint.active {
border-color: #667eea;
background: #f0f4ff;
}
.endpoint-method {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
margin-right: 8px;
}
.method-GET {
background: #28a745;
color: white;
}
.method-POST {
background: #007bff;
color: white;
}
.endpoint-path {
font-size: 13px;
color: #495057;
font-family: 'Monaco', 'Courier New', monospace;
}
.main-area {
padding: 30px;
overflow-y: auto;
max-height: calc(100vh - 200px);
}
.endpoint-details h2 {
font-size: 24px;
margin-bottom: 10px;
color: #1e3c72;
}
.endpoint-details .description {
color: #6c757d;
margin-bottom: 30px;
line-height: 1.6;
}
.params-section {
margin-bottom: 30px;
}
.params-section h3 {
font-size: 16px;
margin-bottom: 15px;
color: #495057;
font-weight: 600;
}
.param-input {
margin-bottom: 15px;
}
.param-input label {
display: block;
font-size: 13px;
font-weight: 600;
color: #495057;
margin-bottom: 5px;
}
.param-input input,
.param-input select,
.param-input textarea {
width: 100%;
padding: 10px;
border: 2px solid #dee2e6;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
}
.param-input input:focus,
.param-input select:focus,
.param-input textarea:focus {
outline: none;
border-color: #667eea;
}
.param-input textarea {
font-family: 'Monaco', 'Courier New', monospace;
min-height: 120px;
}
.param-hint {
font-size: 12px;
color: #868e96;
margin-top: 4px;
}
.execute-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 14px 32px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
margin-top: 20px;
}
.execute-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.execute-btn:active {
transform: translateY(0);
}
.execute-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.response-section {
margin-top: 30px;
border-top: 2px solid #dee2e6;
padding-top: 30px;
}
.response-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.response-header h3 {
font-size: 16px;
color: #495057;
}
.response-meta {
display: flex;
gap: 15px;
font-size: 13px;
}
.status-badge {
padding: 4px 12px;
border-radius: 4px;
font-weight: 600;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-error {
background: #f8d7da;
color: #721c24;
}
.response-time {
color: #6c757d;
font-family: 'Monaco', 'Courier New', monospace;
}
.response-body {
background: #1e1e1e;
color: #d4d4d4;
padding: 20px;
border-radius: 8px;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
max-height: 500px;
overflow-y: auto;
}
.copy-btn {
background: #6c757d;
color: white;
border: none;
padding: 6px 12px;
font-size: 12px;
border-radius: 4px;
cursor: pointer;
}
.copy-btn:hover {
background: #5a6268;
}
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #ffffff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 0.8s linear infinite;
margin-left: 8px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.quick-links {
display: flex;
gap: 15px;
margin-top: 20px;
}
.quick-link {
display: inline-block;
padding: 8px 16px;
background: rgba(255,255,255,0.2);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: background 0.2s;
}
.quick-link:hover {
background: rgba(255,255,255,0.3);
}
pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Omega Watch API Playground</h1>
<p>Interactive API testing and exploration</p>
<div class="quick-links">
<a href="/api/docs/swagger" class="quick-link" target="_blank" rel="noopener noreferrer">Swagger UI</a>
<a href="/api/docs/openapi.json" class="quick-link" target="_blank" rel="noopener noreferrer">OpenAPI Spec</a>
<a href="/docs/API_COMPLETE_GUIDE.md" class="quick-link" target="_blank" rel="noopener noreferrer">Full Guide</a>
</div>
</div>
<div class="content">
<div class="sidebar" id="sidebar"></div>
<div class="main-area" id="mainArea">
<div class="endpoint-details">
<h2>Welcome to the API Playground</h2>
<p class="description">
Select an endpoint from the sidebar to start testing the Omega Watch Price History API.
All requests are made directly from your browser to the production API.
</p>
</div>
</div>
</div>
</div>
<script>
const BASE_URL = 'http://45.61.58.125:7600';
const endpoints = [
{
group: 'Health & System',
items: [
{
method: 'GET',
path: '/api/health',
name: 'Get Health Status',
description: 'Returns detailed system health metrics including uptime, memory usage, cache statistics, and database status.',
params: []
}
]
},
{
group: 'Watches',
items: [
{
method: 'GET',
path: '/api/watches',
name: 'Get All Watches',
description: 'Retrieve a paginated list of watches with optional filtering by series, year, and price range.',
params: [
{ name: 'series', type: 'text', placeholder: 'Speedmaster', hint: 'Filter by series (optional)' },
{ name: 'minYear', type: 'number', placeholder: '1960', hint: 'Minimum year introduced (optional)' },
{ name: 'maxYear', type: 'number', placeholder: '2000', hint: 'Maximum year introduced (optional)' },
{ name: 'minPrice', type: 'number', placeholder: '5000', hint: 'Minimum current price (optional)' },
{ name: 'maxPrice', type: 'number', placeholder: '50000', hint: 'Maximum current price (optional)' },
{ name: 'page', type: 'number', placeholder: '1', hint: 'Page number (default: 1)' },
{ name: 'limit', type: 'number', placeholder: '20', hint: 'Items per page (default: 50, max: 100)' }
]
},
{
method: 'GET',
path: '/api/watches/{id}/history',
name: 'Get Watch History',
description: 'Get complete price history and specifications for a specific watch.',
params: [
{ name: 'id', type: 'text', placeholder: 'speedmaster-moonwatch-1969', hint: 'Watch identifier (required)', pathParam: true }
]
},
{
method: 'GET',
path: '/api/watches/trending',
name: 'Get Trending Watches',
description: 'Returns the most viewed watches based on analytics.',
params: [
{ name: 'limit', type: 'number', placeholder: '10', hint: 'Number of results (default: 10, max: 50)' }
]
}
]
},
{
group: 'Search',
items: [
{
method: 'GET',
path: '/api/search',
name: 'Search Watches',
description: 'Search watches by keyword with advanced filtering options.',
params: [
{ name: 'q', type: 'text', placeholder: 'moonwatch', hint: 'Search query (required)' },
{ name: 'series', type: 'text', placeholder: 'Speedmaster', hint: 'Filter by series (optional)' },
{ name: 'movement', type: 'text', placeholder: 'automatic', hint: 'Filter by movement type (optional)' },
{ name: 'minPrice', type: 'number', placeholder: '10000', hint: 'Minimum price (optional)' },
{ name: 'maxPrice', type: 'number', placeholder: '100000', hint: 'Maximum price (optional)' },
{ name: 'page', type: 'number', placeholder: '1', hint: 'Page number (default: 1)' },
{ name: 'limit', type: 'number', placeholder: '20', hint: 'Items per page (default: 20)' }
]
}
]
},
{
group: 'Analytics',
items: [
{
method: 'GET',
path: '/api/price-predictions/{id}',
name: 'Get Price Predictions',
description: 'Get AI-generated price predictions for next 3 years using linear regression.',
params: [
{ name: 'id', type: 'text', placeholder: 'speedmaster-moonwatch-1969', hint: 'Watch identifier (required)', pathParam: true }
]
},
{
method: 'GET',
path: '/api/statistics',
name: 'Get Market Statistics',
description: 'Get comprehensive market statistics including total watches, average appreciation, and top performers.',
params: []
}
]
},
{
group: 'Collections',
items: [
{
method: 'GET',
path: '/api/collections',
name: 'Get All Collections',
description: 'Get all watch collections with comprehensive statistics.',
params: []
}
]
},
{
group: 'Comparison',
items: [
{
method: 'POST',
path: '/api/compare',
name: 'Compare Watches',
description: 'Compare multiple watches side-by-side (2-10 watches).',
params: [
{
name: 'body',
type: 'json',
placeholder: '{\n "watchIds": [\n "speedmaster-moonwatch-1969",\n "seamaster-300-1957"\n ]\n}',
hint: 'Request body (JSON)'
}
]
}
]
},
{
group: 'Export',
items: [
{
method: 'GET',
path: '/api/export/json',
name: 'Export as JSON',
description: 'Export complete database as JSON.',
params: []
},
{
method: 'GET',
path: '/api/export/csv',
name: 'Export as CSV',
description: 'Export complete database as CSV.',
params: []
}
]
}
];
function renderSidebar() {
const sidebar = document.getElementById('sidebar');
sidebar.innerHTML = '';
endpoints.forEach(group => {
const groupDiv = document.createElement('div');
groupDiv.className = 'endpoint-group';
const groupTitle = document.createElement('h3');
groupTitle.textContent = group.group;
groupDiv.appendChild(groupTitle);
group.items.forEach(endpoint => {
const endpointDiv = document.createElement('div');
endpointDiv.className = 'endpoint';
endpointDiv.innerHTML = `
<span class="endpoint-method method-${endpoint.method}">${endpoint.method}</span>
<span class="endpoint-path">${endpoint.path}</span>
`;
endpointDiv.onclick = () => showEndpoint(endpoint);
groupDiv.appendChild(endpointDiv);
});
sidebar.appendChild(groupDiv);
});
}
function showEndpoint(endpoint) {
// Highlight active endpoint
document.querySelectorAll('.endpoint').forEach(el => el.classList.remove('active'));
event.currentTarget.classList.add('active');
const mainArea = document.getElementById('mainArea');
mainArea.innerHTML = `
<div class="endpoint-details">
<h2>${endpoint.name}</h2>
<p class="description">${endpoint.description}</p>
${endpoint.params.length > 0 ? `
<div class="params-section">
<h3>Parameters</h3>
${endpoint.params.map(param => renderParamInput(param)).join('')}
</div>
` : ''}
<button class="execute-btn" onclick="executeRequest('${endpoint.method}', '${endpoint.path}', ${JSON.stringify(endpoint.params).replace(/"/g, '"')})">
Execute Request
</button>
<div id="responseSection" style="display: none;" class="response-section">
<div class="response-header">
<h3>Response</h3>
<div class="response-meta">
<span id="statusBadge"></span>
<span id="responseTime" class="response-time"></span>
<button class="copy-btn" onclick="copyResponse()">Copy</button>
</div>
</div>
<div class="response-body" id="responseBody"></div>
</div>
</div>
`;
}
function renderParamInput(param) {
if (param.type === 'json') {
return `
<div class="param-input">
<label>${param.name}</label>
<textarea id="param_${param.name}" placeholder="${param.placeholder || ''}">${param.placeholder || ''}</textarea>
<div class="param-hint">${param.hint || ''}</div>
</div>
`;
}
return `
<div class="param-input">
<label>${param.name}${param.pathParam ? ' (path parameter)' : ''}</label>
<input type="${param.type}" id="param_${param.name}" placeholder="${param.placeholder || ''}" ${param.pathParam ? 'required' : ''}>
<div class="param-hint">${param.hint || ''}</div>
</div>
`;
}
async function executeRequest(method, path, params) {
const btn = event.target;
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = 'Executing<span class="loading"></span>';
const startTime = performance.now();
try {
let url = BASE_URL + path;
let body = null;
const queryParams = new URLSearchParams();
// Collect parameters
params.forEach(param => {
const value = document.getElementById(`param_${param.name}`)?.value;
if (!value) return;
if (param.pathParam) {
url = url.replace(`{${param.name}}`, encodeURIComponent(value));
} else if (param.type === 'json') {
body = value;
} else {
queryParams.append(param.name, value);
}
});
if (queryParams.toString()) {
url += '?' + queryParams.toString();
}
const options = { method };
if (body) {
options.headers = { 'Content-Type': 'application/json' };
options.body = body;
}
const response = await fetch(url, options);
const endTime = performance.now();
const responseTime = Math.round(endTime - startTime);
let data;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
displayResponse(response.status, data, responseTime);
} catch (error) {
displayResponse(0, { error: error.message }, 0);
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
function displayResponse(status, data, responseTime) {
const responseSection = document.getElementById('responseSection');
const statusBadge = document.getElementById('statusBadge');
const responseTimeEl = document.getElementById('responseTime');
const responseBody = document.getElementById('responseBody');
responseSection.style.display = 'block';
// Status badge
const isSuccess = status >= 200 && status < 300;
statusBadge.className = 'status-badge ' + (isSuccess ? 'status-success' : 'status-error');
statusBadge.textContent = status > 0 ? `HTTP ${status}` : 'ERROR';
// Response time
responseTimeEl.textContent = responseTime > 0 ? `${responseTime}ms` : '';
// Response body
const formatted = typeof data === 'string'
? data
: JSON.stringify(data, null, 2);
responseBody.textContent = formatted;
// Scroll to response
responseSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function copyResponse() {
const responseBody = document.getElementById('responseBody');
navigator.clipboard.writeText(responseBody.textContent);
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
}
// Initialize
renderSidebar();
// Auto-select first endpoint
if (endpoints[0]?.items[0]) {
setTimeout(() => {
const firstEndpoint = document.querySelector('.endpoint');
if (firstEndpoint) {
firstEndpoint.click();
}
}, 100);
}
</script>
</body>
</html>