- Implement full web interface with Go html/template server - Add GX component library (buttons, dialogs, tables, forms, etc.) - Create scene/performer/studio/movie detail and listing pages - Add Adult Empire scraper for additional metadata sources - Implement movie support with database schema - Add import and sync services for data management - Include comprehensive API and frontend documentation - Add custom color scheme and responsive layout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
979 lines
23 KiB
Markdown
979 lines
23 KiB
Markdown
# Goondex Frontend API Guide
|
|
|
|
**For Frontend Developers**
|
|
|
|
This guide explains how to interact with the Goondex API using JavaScript. No backend knowledge required - just JavaScript, HTML, CSS, and Bootstrap skills!
|
|
|
|
## Table of Contents
|
|
|
|
1. [Getting Started](#getting-started)
|
|
2. [Base URL](#base-url)
|
|
3. [Data Models](#data-models)
|
|
4. [API Endpoints](#api-endpoints)
|
|
5. [Common Workflows](#common-workflows)
|
|
6. [Error Handling](#error-handling)
|
|
7. [Real-time Progress Updates](#real-time-progress-updates)
|
|
8. [Complete Examples](#complete-examples)
|
|
|
|
---
|
|
|
|
## Getting Started
|
|
|
|
All API endpoints return JSON data. You can use the `fetch` API to make requests from your JavaScript code.
|
|
|
|
### Basic API Response Format
|
|
|
|
All API responses follow this structure:
|
|
|
|
```javascript
|
|
{
|
|
"success": true, // boolean - whether the operation succeeded
|
|
"message": "Success text", // string - human-readable message
|
|
"data": { ... } // object - actual data (optional)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Base URL
|
|
|
|
The API server runs at: `http://localhost:8080` (by default)
|
|
|
|
All endpoints are prefixed with this URL.
|
|
|
|
---
|
|
|
|
## Data Models
|
|
|
|
### Performer
|
|
|
|
```javascript
|
|
{
|
|
"id": 123,
|
|
"name": "Performer Name",
|
|
"aliases": "Alias1, Alias2",
|
|
|
|
// Physical Attributes
|
|
"gender": "female", // male/female/trans/other
|
|
"birthday": "1995-03-15", // YYYY-MM-DD
|
|
"astrology": "Pisces",
|
|
"birthplace": "Los Angeles, CA",
|
|
"ethnicity": "Caucasian",
|
|
"nationality": "US", // ISO country code (US, GB, FR, etc.)
|
|
"country": "United States",
|
|
"eye_color": "Blue",
|
|
"hair_color": "Blonde",
|
|
"height": 165, // centimeters
|
|
"weight": 55, // kilograms
|
|
"measurements": "34C-24-36",
|
|
"cup_size": "34C",
|
|
"tattoo_description": "Dragon on left shoulder",
|
|
"piercing_description": "Nose piercing",
|
|
"boob_job": "False", // "True" or "False" as string
|
|
|
|
// Career
|
|
"career": "2015-2023",
|
|
"career_start_year": 2015,
|
|
"career_end_year": 2023,
|
|
"date_of_death": "", // YYYY-MM-DD if applicable
|
|
"active": true,
|
|
|
|
// Media
|
|
"image_path": "/path/to/image.jpg",
|
|
"image_url": "https://example.com/image.jpg",
|
|
"poster_url": "https://example.com/poster.jpg",
|
|
"bio": "Biography text...",
|
|
|
|
// Metadata
|
|
"source": "tpdb",
|
|
"source_id": "abc-123-def",
|
|
"source_numeric_id": 456,
|
|
"created_at": "2024-01-01T12:00:00Z",
|
|
"updated_at": "2024-01-02T12:00:00Z"
|
|
}
|
|
```
|
|
|
|
### Studio
|
|
|
|
```javascript
|
|
{
|
|
"id": 456,
|
|
"name": "Studio Name",
|
|
"parent_id": 123, // null if no parent studio
|
|
"image_path": "/path/to/logo.jpg",
|
|
"image_url": "https://example.com/logo.jpg",
|
|
"description": "Studio description...",
|
|
"source": "tpdb",
|
|
"source_id": "xyz-789",
|
|
"created_at": "2024-01-01T12:00:00Z",
|
|
"updated_at": "2024-01-02T12:00:00Z"
|
|
}
|
|
```
|
|
|
|
### Scene
|
|
|
|
```javascript
|
|
{
|
|
"id": 789,
|
|
"title": "Scene Title",
|
|
"code": "SCENE-001", // DVD code or scene identifier
|
|
"date": "2024-01-15", // Release date YYYY-MM-DD
|
|
"studio_id": 456,
|
|
"description": "Scene description...",
|
|
"image_path": "/path/to/thumbnail.jpg",
|
|
"image_url": "https://example.com/thumbnail.jpg",
|
|
"director": "Director Name",
|
|
"url": "https://example.com/scene",
|
|
"source": "tpdb",
|
|
"source_id": "scene-123",
|
|
"created_at": "2024-01-01T12:00:00Z",
|
|
"updated_at": "2024-01-02T12:00:00Z",
|
|
|
|
// Relationships (when populated)
|
|
"performers": [ /* array of Performer objects */ ],
|
|
"tags": [ /* array of Tag objects */ ],
|
|
"studio": { /* Studio object */ }
|
|
}
|
|
```
|
|
|
|
### Movie
|
|
|
|
```javascript
|
|
{
|
|
"id": 321,
|
|
"title": "Movie Title",
|
|
"date": "2024-01-01", // Release date
|
|
"studio_id": 456,
|
|
"description": "Movie description...",
|
|
"director": "Director Name",
|
|
"duration": 120, // Duration in minutes
|
|
"image_path": "/path/to/cover.jpg",
|
|
"image_url": "https://example.com/cover.jpg",
|
|
"back_image_url": "https://example.com/back-cover.jpg",
|
|
"url": "https://example.com/movie",
|
|
"source": "tpdb",
|
|
"source_id": "movie-456",
|
|
"created_at": "2024-01-01T12:00:00Z",
|
|
"updated_at": "2024-01-02T12:00:00Z",
|
|
|
|
// Relationships (when populated)
|
|
"scenes": [ /* array of Scene objects */ ],
|
|
"performers": [ /* array of Performer objects */ ],
|
|
"tags": [ /* array of Tag objects */ ],
|
|
"studio": { /* Studio object */ }
|
|
}
|
|
```
|
|
|
|
### Tag
|
|
|
|
```javascript
|
|
{
|
|
"id": 111,
|
|
"name": "Tag Name",
|
|
"category_id": 5,
|
|
"aliases": "Alias1, Alias2",
|
|
"description": "Tag description...",
|
|
"source": "tpdb",
|
|
"source_id": "tag-789",
|
|
"created_at": "2024-01-01T12:00:00Z",
|
|
"updated_at": "2024-01-02T12:00:00Z"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
### Search & Import
|
|
|
|
#### 1. Import Performer by Search
|
|
|
|
**Endpoint:** `POST /api/import/performer`
|
|
|
|
**Description:** Search for a performer and import all matching results from TPDB.
|
|
|
|
**Request Body:**
|
|
```javascript
|
|
{
|
|
"query": "performer name"
|
|
}
|
|
```
|
|
|
|
**Example:**
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/import/performer', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
query: 'Jane Doe'
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
// result = {
|
|
// "success": true,
|
|
// "message": "Imported 5 performer(s)",
|
|
// "data": { "imported": 5, "found": 5 }
|
|
// }
|
|
```
|
|
|
|
#### 2. Import Studio by Search
|
|
|
|
**Endpoint:** `POST /api/import/studio`
|
|
|
|
**Request Body:**
|
|
```javascript
|
|
{
|
|
"query": "studio name"
|
|
}
|
|
```
|
|
|
|
**Example:**
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/import/studio', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query: 'Brazzers' })
|
|
});
|
|
|
|
const result = await response.json();
|
|
```
|
|
|
|
#### 3. Import Scene by Search
|
|
|
|
**Endpoint:** `POST /api/import/scene`
|
|
|
|
**Description:** Search for a scene and import all matching results. This also imports associated performers, studio, and tags.
|
|
|
|
**Request Body:**
|
|
```javascript
|
|
{
|
|
"query": "scene title"
|
|
}
|
|
```
|
|
|
|
**Example:**
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/import/scene', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query: 'Scene Title' })
|
|
});
|
|
|
|
const result = await response.json();
|
|
```
|
|
|
|
### Bulk Import
|
|
|
|
#### 4. Bulk Import All
|
|
|
|
**Endpoint:** `POST /api/import/all`
|
|
|
|
**Description:** Import all performers, studios, and scenes from your local database. This fetches full metadata from TPDB.
|
|
|
|
**No Request Body Required**
|
|
|
|
**Example:**
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/import/all', {
|
|
method: 'POST'
|
|
});
|
|
|
|
const result = await response.json();
|
|
// result.data contains import statistics
|
|
```
|
|
|
|
#### 5. Bulk Import All Performers
|
|
|
|
**Endpoint:** `POST /api/import/all-performers`
|
|
|
|
**Example:**
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/import/all-performers', {
|
|
method: 'POST'
|
|
});
|
|
|
|
const result = await response.json();
|
|
// result = {
|
|
// "success": true,
|
|
// "message": "Imported 150/200 performers",
|
|
// "data": {
|
|
// "total": 200,
|
|
// "imported": 150,
|
|
// "skipped": 50,
|
|
// "errors": 0
|
|
// }
|
|
// }
|
|
```
|
|
|
|
#### 6. Bulk Import All Studios
|
|
|
|
**Endpoint:** `POST /api/import/all-studios`
|
|
|
|
**Example:**
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/import/all-studios', {
|
|
method: 'POST'
|
|
});
|
|
|
|
const result = await response.json();
|
|
```
|
|
|
|
#### 7. Bulk Import All Scenes
|
|
|
|
**Endpoint:** `POST /api/import/all-scenes`
|
|
|
|
**Example:**
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/import/all-scenes', {
|
|
method: 'POST'
|
|
});
|
|
|
|
const result = await response.json();
|
|
```
|
|
|
|
### Sync
|
|
|
|
#### 8. Sync All Data
|
|
|
|
**Endpoint:** `POST /api/sync`
|
|
|
|
**Description:** Synchronize all data with TPDB to get the latest updates.
|
|
|
|
**Request Body (Optional):**
|
|
```javascript
|
|
{
|
|
"force": false // Set to true to force sync even if recently synced
|
|
}
|
|
```
|
|
|
|
**Example:**
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/sync', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ force: true })
|
|
});
|
|
|
|
const result = await response.json();
|
|
```
|
|
|
|
#### 9. Get Sync Status
|
|
|
|
**Endpoint:** `GET /api/sync/status`
|
|
|
|
**Description:** Get the last sync time for all entities.
|
|
|
|
**Example:**
|
|
```javascript
|
|
const response = await fetch('http://localhost:8080/api/sync/status');
|
|
const result = await response.json();
|
|
// result.data contains sync status for each entity type
|
|
```
|
|
|
|
### Global Search
|
|
|
|
#### 10. Search Everything
|
|
|
|
**Endpoint:** `GET /api/search?q=query`
|
|
|
|
**Description:** Search across performers, studios, scenes, and tags simultaneously.
|
|
|
|
**Example:**
|
|
```javascript
|
|
const query = 'search term';
|
|
const response = await fetch(`http://localhost:8080/api/search?q=${encodeURIComponent(query)}`);
|
|
const result = await response.json();
|
|
|
|
// result.data = {
|
|
// "performers": [...],
|
|
// "studios": [...],
|
|
// "scenes": [...],
|
|
// "tags": [...],
|
|
// "total": 25
|
|
// }
|
|
```
|
|
|
|
---
|
|
|
|
## Real-time Progress Updates
|
|
|
|
For bulk imports, there are special endpoints that provide real-time progress updates using Server-Sent Events (SSE).
|
|
|
|
### Bulk Import with Progress
|
|
|
|
These endpoints stream progress updates as the import happens:
|
|
|
|
- `GET /api/import/all-performers/progress`
|
|
- `GET /api/import/all-studios/progress`
|
|
- `GET /api/import/all-scenes/progress`
|
|
|
|
**Example with EventSource:**
|
|
|
|
```javascript
|
|
// Create an EventSource to listen for progress updates
|
|
const eventSource = new EventSource('http://localhost:8080/api/import/all-performers/progress');
|
|
|
|
eventSource.onmessage = function(event) {
|
|
const update = JSON.parse(event.data);
|
|
|
|
if (update.error) {
|
|
console.error('Import error:', update.error);
|
|
eventSource.close();
|
|
return;
|
|
}
|
|
|
|
if (update.complete) {
|
|
console.log('Import complete!', update.result);
|
|
eventSource.close();
|
|
return;
|
|
}
|
|
|
|
// Progress update
|
|
console.log(`Progress: ${update.current}/${update.total}`);
|
|
console.log(`Current item: ${update.name}`);
|
|
console.log(`Status: ${update.status}`);
|
|
|
|
// Update UI
|
|
updateProgressBar(update.current, update.total);
|
|
};
|
|
|
|
eventSource.onerror = function(error) {
|
|
console.error('EventSource error:', error);
|
|
eventSource.close();
|
|
};
|
|
```
|
|
|
|
**Progress Update Format:**
|
|
|
|
```javascript
|
|
{
|
|
"current": 15, // Current item number
|
|
"total": 100, // Total items to process
|
|
"name": "Jane Doe", // Name of current item being processed
|
|
"status": "importing" // Status message
|
|
}
|
|
```
|
|
|
|
**Completion Format:**
|
|
|
|
```javascript
|
|
{
|
|
"complete": true,
|
|
"result": {
|
|
"total": 100,
|
|
"imported": 95,
|
|
"skipped": 4,
|
|
"errors": 1
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Common Workflows
|
|
|
|
### 1. Search and Display Performers
|
|
|
|
```javascript
|
|
// Search for performers
|
|
async function searchPerformers(searchQuery) {
|
|
const response = await fetch(
|
|
`http://localhost:8080/api/search?q=${encodeURIComponent(searchQuery)}`
|
|
);
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
return result.data.performers;
|
|
}
|
|
throw new Error(result.message);
|
|
}
|
|
|
|
// Display in HTML
|
|
async function displayPerformers() {
|
|
const performers = await searchPerformers('jane');
|
|
|
|
const container = document.getElementById('performers-list');
|
|
container.innerHTML = performers.map(p => `
|
|
<div class="card">
|
|
<img src="${p.image_url}" class="card-img-top" alt="${p.name}">
|
|
<div class="card-body">
|
|
<h5 class="card-title">${p.name}</h5>
|
|
<p class="card-text">
|
|
${p.nationality ? getFlagEmoji(p.nationality) : ''}
|
|
${p.gender || 'Unknown'}
|
|
</p>
|
|
<a href="/performers/${p.id}" class="btn btn-primary">View Details</a>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Helper: Convert country code to flag emoji
|
|
function getFlagEmoji(countryCode) {
|
|
const codePoints = countryCode
|
|
.toUpperCase()
|
|
.split('')
|
|
.map(char => 127397 + char.charCodeAt());
|
|
return String.fromCodePoint(...codePoints);
|
|
}
|
|
```
|
|
|
|
### 2. Import Data with Progress Bar
|
|
|
|
```javascript
|
|
async function importPerformersWithProgress() {
|
|
const progressBar = document.getElementById('progress-bar');
|
|
const statusText = document.getElementById('status-text');
|
|
|
|
const eventSource = new EventSource(
|
|
'http://localhost:8080/api/import/all-performers/progress'
|
|
);
|
|
|
|
eventSource.onmessage = function(event) {
|
|
const update = JSON.parse(event.data);
|
|
|
|
if (update.error) {
|
|
statusText.textContent = `Error: ${update.error}`;
|
|
progressBar.style.width = '100%';
|
|
progressBar.classList.add('bg-danger');
|
|
eventSource.close();
|
|
return;
|
|
}
|
|
|
|
if (update.complete) {
|
|
statusText.textContent =
|
|
`Complete! Imported ${update.result.imported}/${update.result.total}`;
|
|
progressBar.style.width = '100%';
|
|
progressBar.classList.add('bg-success');
|
|
eventSource.close();
|
|
return;
|
|
}
|
|
|
|
// Update progress
|
|
const percent = (update.current / update.total) * 100;
|
|
progressBar.style.width = `${percent}%`;
|
|
statusText.textContent =
|
|
`Importing ${update.name} (${update.current}/${update.total})`;
|
|
};
|
|
|
|
eventSource.onerror = function(error) {
|
|
statusText.textContent = 'Connection error';
|
|
progressBar.classList.add('bg-danger');
|
|
eventSource.close();
|
|
};
|
|
}
|
|
```
|
|
|
|
### 3. Sync Data
|
|
|
|
```javascript
|
|
async function syncAllData(forceSync = false) {
|
|
const response = await fetch('http://localhost:8080/api/sync', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ force: forceSync })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
console.log('Sync completed:', result.data);
|
|
return result.data;
|
|
}
|
|
throw new Error(result.message);
|
|
}
|
|
|
|
// Check last sync status
|
|
async function checkSyncStatus() {
|
|
const response = await fetch('http://localhost:8080/api/sync/status');
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
console.log('Last sync times:', result.data);
|
|
return result.data;
|
|
}
|
|
throw new Error(result.message);
|
|
}
|
|
```
|
|
|
|
### 4. Search and Import New Performer
|
|
|
|
```javascript
|
|
async function importNewPerformer(performerName) {
|
|
const response = await fetch('http://localhost:8080/api/import/performer', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query: performerName })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
alert(`Successfully imported ${result.data.imported} performer(s)`);
|
|
return result.data;
|
|
}
|
|
alert(`Failed: ${result.message}`);
|
|
throw new Error(result.message);
|
|
}
|
|
|
|
// Usage with a form
|
|
document.getElementById('import-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const performerName = document.getElementById('performer-name').value;
|
|
await importNewPerformer(performerName);
|
|
// Refresh the performer list
|
|
location.reload();
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
### Best Practices
|
|
|
|
Always check the `success` field in the response:
|
|
|
|
```javascript
|
|
async function safeApiCall(url, options = {}) {
|
|
try {
|
|
const response = await fetch(url, options);
|
|
|
|
// Check HTTP status
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// Check API success field
|
|
if (!result.success) {
|
|
throw new Error(result.message);
|
|
}
|
|
|
|
return result.data;
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
try {
|
|
const performers = await safeApiCall('http://localhost:8080/api/search?q=jane');
|
|
console.log('Performers:', performers);
|
|
} catch (error) {
|
|
alert(`Error: ${error.message}`);
|
|
}
|
|
```
|
|
|
|
### Common Error Responses
|
|
|
|
**API Key Not Configured:**
|
|
```javascript
|
|
{
|
|
"success": false,
|
|
"message": "TPDB_API_KEY not configured"
|
|
}
|
|
```
|
|
|
|
**No Results Found:**
|
|
```javascript
|
|
{
|
|
"success": false,
|
|
"message": "No performers found"
|
|
}
|
|
```
|
|
|
|
**Search Failed:**
|
|
```javascript
|
|
{
|
|
"success": false,
|
|
"message": "Search failed: connection timeout"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Complete Examples
|
|
|
|
### Example 1: Search Form with Results
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Goondex Search</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
</head>
|
|
<body>
|
|
<div class="container mt-5">
|
|
<h1>Search Goondex</h1>
|
|
|
|
<form id="search-form" class="mb-4">
|
|
<div class="input-group">
|
|
<input type="text" id="search-input" class="form-control" placeholder="Search...">
|
|
<button type="submit" class="btn btn-primary">Search</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div id="results"></div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = 'http://localhost:8080';
|
|
|
|
document.getElementById('search-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const query = document.getElementById('search-input').value;
|
|
const resultsDiv = document.getElementById('results');
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/search?q=${encodeURIComponent(query)}`);
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
resultsDiv.innerHTML = `<div class="alert alert-danger">${result.message}</div>`;
|
|
return;
|
|
}
|
|
|
|
const data = result.data;
|
|
|
|
resultsDiv.innerHTML = `
|
|
<h2>Results (${data.total} total)</h2>
|
|
|
|
<h3>Performers (${data.performers.length})</h3>
|
|
<div class="row">
|
|
${data.performers.map(p => `
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card">
|
|
<img src="${p.image_url || '/static/placeholder.jpg'}" class="card-img-top">
|
|
<div class="card-body">
|
|
<h5 class="card-title">${p.name}</h5>
|
|
<p class="card-text">${p.nationality || ''}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
|
|
<h3>Studios (${data.studios.length})</h3>
|
|
<ul class="list-group">
|
|
${data.studios.map(s => `
|
|
<li class="list-group-item">${s.name}</li>
|
|
`).join('')}
|
|
</ul>
|
|
`;
|
|
} catch (error) {
|
|
resultsDiv.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Example 2: Import Progress with Bootstrap
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Import Performers</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
</head>
|
|
<body>
|
|
<div class="container mt-5">
|
|
<h1>Import All Performers</h1>
|
|
|
|
<button id="start-import" class="btn btn-primary mb-4">Start Import</button>
|
|
|
|
<div class="progress mb-2" style="display: none;">
|
|
<div id="progress-bar" class="progress-bar" role="progressbar" style="width: 0%">0%</div>
|
|
</div>
|
|
|
|
<div id="status" class="alert alert-info" style="display: none;"></div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = 'http://localhost:8080';
|
|
|
|
document.getElementById('start-import').addEventListener('click', () => {
|
|
const progressDiv = document.querySelector('.progress');
|
|
const progressBar = document.getElementById('progress-bar');
|
|
const statusDiv = document.getElementById('status');
|
|
const button = document.getElementById('start-import');
|
|
|
|
button.disabled = true;
|
|
progressDiv.style.display = 'block';
|
|
statusDiv.style.display = 'block';
|
|
|
|
const eventSource = new EventSource(`${API_BASE}/api/import/all-performers/progress`);
|
|
|
|
eventSource.onmessage = function(event) {
|
|
const update = JSON.parse(event.data);
|
|
|
|
if (update.error) {
|
|
statusDiv.className = 'alert alert-danger';
|
|
statusDiv.textContent = `Error: ${update.error}`;
|
|
progressBar.className = 'progress-bar bg-danger';
|
|
progressBar.style.width = '100%';
|
|
button.disabled = false;
|
|
eventSource.close();
|
|
return;
|
|
}
|
|
|
|
if (update.complete) {
|
|
const result = update.result;
|
|
statusDiv.className = 'alert alert-success';
|
|
statusDiv.textContent =
|
|
`Complete! Imported ${result.imported}/${result.total} performers ` +
|
|
`(${result.skipped} skipped, ${result.errors} errors)`;
|
|
progressBar.className = 'progress-bar bg-success';
|
|
progressBar.style.width = '100%';
|
|
progressBar.textContent = '100%';
|
|
button.disabled = false;
|
|
eventSource.close();
|
|
return;
|
|
}
|
|
|
|
const percent = Math.round((update.current / update.total) * 100);
|
|
progressBar.style.width = `${percent}%`;
|
|
progressBar.textContent = `${percent}%`;
|
|
statusDiv.className = 'alert alert-info';
|
|
statusDiv.textContent =
|
|
`Importing: ${update.name} (${update.current}/${update.total})`;
|
|
};
|
|
|
|
eventSource.onerror = function() {
|
|
statusDiv.className = 'alert alert-danger';
|
|
statusDiv.textContent = 'Connection error';
|
|
progressBar.className = 'progress-bar bg-danger';
|
|
button.disabled = false;
|
|
eventSource.close();
|
|
};
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
---
|
|
|
|
## Tips for Frontend Developers
|
|
|
|
### 1. Always Use `encodeURIComponent` for Query Parameters
|
|
|
|
```javascript
|
|
// Good
|
|
const query = 'Jane Doe & Associates';
|
|
fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
|
|
|
// Bad - will break with special characters
|
|
fetch(`/api/search?q=${query}`);
|
|
```
|
|
|
|
### 2. Handle Image Loading Errors
|
|
|
|
```javascript
|
|
<img
|
|
src="${performer.image_url}"
|
|
onerror="this.src='/static/placeholder.jpg'"
|
|
alt="${performer.name}"
|
|
>
|
|
```
|
|
|
|
### 3. Use Bootstrap Classes for Quick Styling
|
|
|
|
```html
|
|
<!-- Alert messages -->
|
|
<div class="alert alert-success">Success!</div>
|
|
<div class="alert alert-danger">Error!</div>
|
|
<div class="alert alert-warning">Warning!</div>
|
|
|
|
<!-- Loading spinner -->
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
|
|
<!-- Progress bar -->
|
|
<div class="progress">
|
|
<div class="progress-bar" style="width: 75%">75%</div>
|
|
</div>
|
|
```
|
|
|
|
### 4. Debounce Search Input
|
|
|
|
```javascript
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function(...args) {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
};
|
|
}
|
|
|
|
// Usage
|
|
const searchInput = document.getElementById('search');
|
|
const debouncedSearch = debounce(performSearch, 300);
|
|
searchInput.addEventListener('input', (e) => debouncedSearch(e.target.value));
|
|
```
|
|
|
|
### 5. Format Dates for Display
|
|
|
|
```javascript
|
|
function formatDate(dateString) {
|
|
if (!dateString) return 'Unknown';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
|
|
// Usage
|
|
console.log(formatDate('2024-01-15')); // "January 15, 2024"
|
|
```
|
|
|
|
### 6. Calculate Age from Birthday
|
|
|
|
```javascript
|
|
function calculateAge(birthday) {
|
|
if (!birthday) return null;
|
|
const birthDate = new Date(birthday);
|
|
const today = new Date();
|
|
let age = today.getFullYear() - birthDate.getFullYear();
|
|
const monthDiff = today.getMonth() - birthDate.getMonth();
|
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
|
age--;
|
|
}
|
|
return age;
|
|
}
|
|
|
|
// Usage
|
|
const age = calculateAge('1995-03-15');
|
|
console.log(`Age: ${age}`);
|
|
```
|
|
|
|
---
|
|
|
|
## Need Help?
|
|
|
|
If you have questions about the API or need clarification:
|
|
|
|
1. Check the data model structures above
|
|
2. Look at the complete examples
|
|
3. Test endpoints using browser DevTools Network tab
|
|
4. Consult your backend developer if you need custom endpoints
|
|
|
|
Happy coding!
|