← 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, '&quot;')})">
                        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>