- 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>
1166 lines
30 KiB
Markdown
1166 lines
30 KiB
Markdown
# Goondex HTML/CSS Templates Guide
|
|
|
|
**For Frontend Developers - HTML/CSS Focus**
|
|
|
|
This guide provides complete, ready-to-use HTML templates with Bootstrap styling. All JavaScript is included as simple copy-paste snippets with detailed comments.
|
|
|
|
## Getting Started
|
|
|
|
All you need to know:
|
|
1. Copy the HTML template you need
|
|
2. The JavaScript is already included in `<script>` tags - no modifications needed
|
|
3. Customize the CSS/Bootstrap classes for styling
|
|
4. Use Bootstrap 5 components for quick UI elements
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Complete Search Page](#complete-search-page)
|
|
2. [Performer Import Form](#performer-import-form)
|
|
3. [Bulk Import with Progress](#bulk-import-with-progress)
|
|
4. [Simple Data Display](#simple-data-display)
|
|
5. [Bootstrap Component Reference](#bootstrap-component-reference)
|
|
|
|
---
|
|
|
|
## Complete Search Page
|
|
|
|
**File: `search.html`**
|
|
|
|
Copy this entire file - it's ready to use!
|
|
|
|
```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>
|
|
|
|
<!-- Bootstrap CSS -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
|
|
<!-- Custom CSS -->
|
|
<style>
|
|
body {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.search-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.performer-card {
|
|
transition: transform 0.2s;
|
|
height: 100%;
|
|
}
|
|
|
|
.performer-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.performer-image {
|
|
height: 300px;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.no-results {
|
|
text-align: center;
|
|
padding: 3rem;
|
|
color: #6c757d;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Navigation -->
|
|
<nav class="navbar navbar-dark bg-dark">
|
|
<div class="container-fluid">
|
|
<a class="navbar-brand" href="/">Goondex</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main Content -->
|
|
<div class="search-container">
|
|
<h1 class="mb-4">Search</h1>
|
|
|
|
<!-- Search Form -->
|
|
<form id="search-form" class="mb-5">
|
|
<div class="input-group input-group-lg">
|
|
<input
|
|
type="text"
|
|
id="search-input"
|
|
class="form-control"
|
|
placeholder="Search performers, studios, scenes..."
|
|
required
|
|
>
|
|
<button type="submit" class="btn btn-primary px-5">
|
|
Search
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Loading Spinner (hidden by default) -->
|
|
<div id="loading" class="text-center" style="display: none;">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2">Searching...</p>
|
|
</div>
|
|
|
|
<!-- Results Container -->
|
|
<div id="results"></div>
|
|
</div>
|
|
|
|
<!-- JavaScript -->
|
|
<script>
|
|
// Configuration - change this if your server runs on a different port
|
|
const API_BASE = 'http://localhost:8080';
|
|
|
|
// Get elements
|
|
const form = document.getElementById('search-form');
|
|
const input = document.getElementById('search-input');
|
|
const loading = document.getElementById('loading');
|
|
const results = document.getElementById('results');
|
|
|
|
// When form is submitted
|
|
form.addEventListener('submit', async function(event) {
|
|
event.preventDefault(); // Don't reload the page
|
|
|
|
const searchTerm = input.value.trim();
|
|
if (!searchTerm) return;
|
|
|
|
// Show loading, hide results
|
|
loading.style.display = 'block';
|
|
results.innerHTML = '';
|
|
|
|
try {
|
|
// Make API request
|
|
const response = await fetch(
|
|
`${API_BASE}/api/search?q=${encodeURIComponent(searchTerm)}`
|
|
);
|
|
const data = await response.json();
|
|
|
|
// Hide loading
|
|
loading.style.display = 'none';
|
|
|
|
// Check if successful
|
|
if (!data.success) {
|
|
results.innerHTML = `
|
|
<div class="alert alert-danger">
|
|
${data.message}
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Display results
|
|
displayResults(data.data);
|
|
|
|
} catch (error) {
|
|
loading.style.display = 'none';
|
|
results.innerHTML = `
|
|
<div class="alert alert-danger">
|
|
Error: ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
// Function to display search results
|
|
function displayResults(data) {
|
|
const totalResults = data.total;
|
|
|
|
if (totalResults === 0) {
|
|
results.innerHTML = `
|
|
<div class="no-results">
|
|
<h3>No results found</h3>
|
|
<p>Try a different search term</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = `<h2 class="mb-4">${totalResults} Results Found</h2>`;
|
|
|
|
// Performers Section
|
|
if (data.performers && data.performers.length > 0) {
|
|
html += `
|
|
<h3 class="mt-4 mb-3">Performers (${data.performers.length})</h3>
|
|
<div class="row row-cols-1 row-cols-md-3 row-cols-lg-4 g-4 mb-5">
|
|
`;
|
|
|
|
data.performers.forEach(performer => {
|
|
const imageUrl = performer.image_url || '/static/placeholder.jpg';
|
|
const nationality = performer.nationality ? getFlagEmoji(performer.nationality) : '';
|
|
|
|
html += `
|
|
<div class="col">
|
|
<div class="card performer-card">
|
|
<img
|
|
src="${imageUrl}"
|
|
class="card-img-top performer-image"
|
|
alt="${performer.name}"
|
|
onerror="this.src='/static/placeholder.jpg'"
|
|
>
|
|
<div class="card-body">
|
|
<h5 class="card-title">${performer.name}</h5>
|
|
<p class="card-text">
|
|
${nationality} ${performer.gender || ''}
|
|
</p>
|
|
<a href="/performers/${performer.id}" class="btn btn-sm btn-primary">
|
|
View Profile
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += `</div>`;
|
|
}
|
|
|
|
// Studios Section
|
|
if (data.studios && data.studios.length > 0) {
|
|
html += `
|
|
<h3 class="mt-4 mb-3">Studios (${data.studios.length})</h3>
|
|
<div class="list-group mb-5">
|
|
`;
|
|
|
|
data.studios.forEach(studio => {
|
|
html += `
|
|
<a href="/studios/${studio.id}" class="list-group-item list-group-item-action">
|
|
<div class="d-flex w-100 justify-content-between">
|
|
<h5 class="mb-1">${studio.name}</h5>
|
|
</div>
|
|
${studio.description ? `<p class="mb-1">${studio.description}</p>` : ''}
|
|
</a>
|
|
`;
|
|
});
|
|
|
|
html += `</div>`;
|
|
}
|
|
|
|
// Scenes Section
|
|
if (data.scenes && data.scenes.length > 0) {
|
|
html += `
|
|
<h3 class="mt-4 mb-3">Scenes (${data.scenes.length})</h3>
|
|
<div class="list-group mb-5">
|
|
`;
|
|
|
|
data.scenes.forEach(scene => {
|
|
html += `
|
|
<a href="/scenes/${scene.id}" class="list-group-item list-group-item-action">
|
|
<div class="d-flex w-100 justify-content-between">
|
|
<h5 class="mb-1">${scene.title}</h5>
|
|
${scene.date ? `<small>${scene.date}</small>` : ''}
|
|
</div>
|
|
${scene.description ? `<p class="mb-1">${scene.description.substring(0, 200)}...</p>` : ''}
|
|
</a>
|
|
`;
|
|
});
|
|
|
|
html += `</div>`;
|
|
}
|
|
|
|
results.innerHTML = html;
|
|
}
|
|
|
|
// Helper function: Convert country code to flag emoji
|
|
function getFlagEmoji(countryCode) {
|
|
const codePoints = countryCode
|
|
.toUpperCase()
|
|
.split('')
|
|
.map(char => 127397 + char.charCodeAt());
|
|
return String.fromCodePoint(...codePoints);
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
---
|
|
|
|
## Performer Import Form
|
|
|
|
**File: `import-performer.html`**
|
|
|
|
Simple form to search and import performers from TPDB.
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Import Performer - Goondex</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
|
|
<style>
|
|
.import-container {
|
|
max-width: 600px;
|
|
margin: 3rem auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.success-message {
|
|
animation: slideDown 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes slideDown {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<nav class="navbar navbar-dark bg-dark">
|
|
<div class="container-fluid">
|
|
<a class="navbar-brand" href="/">Goondex</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="import-container">
|
|
<h1 class="mb-4">Import Performer</h1>
|
|
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="card-text text-muted">
|
|
Search for a performer by name. All matching results from TPDB will be imported.
|
|
</p>
|
|
|
|
<form id="import-form">
|
|
<div class="mb-3">
|
|
<label for="performer-name" class="form-label">Performer Name</label>
|
|
<input
|
|
type="text"
|
|
class="form-control form-control-lg"
|
|
id="performer-name"
|
|
placeholder="Enter performer name..."
|
|
required
|
|
>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary btn-lg w-100" id="submit-btn">
|
|
<span id="button-text">Import Performer</span>
|
|
<span id="button-spinner" class="spinner-border spinner-border-sm" style="display: none;"></span>
|
|
</button>
|
|
</form>
|
|
|
|
<!-- Result message (hidden by default) -->
|
|
<div id="result-message" class="mt-3" style="display: none;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = 'http://localhost:8080';
|
|
|
|
const form = document.getElementById('import-form');
|
|
const input = document.getElementById('performer-name');
|
|
const submitBtn = document.getElementById('submit-btn');
|
|
const buttonText = document.getElementById('button-text');
|
|
const buttonSpinner = document.getElementById('button-spinner');
|
|
const resultMessage = document.getElementById('result-message');
|
|
|
|
form.addEventListener('submit', async function(event) {
|
|
event.preventDefault();
|
|
|
|
const performerName = input.value.trim();
|
|
if (!performerName) return;
|
|
|
|
// Show loading state
|
|
submitBtn.disabled = true;
|
|
buttonText.textContent = 'Importing...';
|
|
buttonSpinner.style.display = 'inline-block';
|
|
resultMessage.style.display = 'none';
|
|
|
|
try {
|
|
// Make API request
|
|
const response = await fetch(`${API_BASE}/api/import/performer`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
query: performerName
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Reset button
|
|
submitBtn.disabled = false;
|
|
buttonText.textContent = 'Import Performer';
|
|
buttonSpinner.style.display = 'none';
|
|
|
|
// Show result
|
|
if (data.success) {
|
|
resultMessage.className = 'alert alert-success success-message mt-3';
|
|
resultMessage.innerHTML = `
|
|
<strong>Success!</strong> ${data.message}<br>
|
|
<small>Imported ${data.data.imported} out of ${data.data.found} found</small>
|
|
`;
|
|
resultMessage.style.display = 'block';
|
|
|
|
// Clear form
|
|
input.value = '';
|
|
|
|
} else {
|
|
resultMessage.className = 'alert alert-danger mt-3';
|
|
resultMessage.innerHTML = `<strong>Error:</strong> ${data.message}`;
|
|
resultMessage.style.display = 'block';
|
|
}
|
|
|
|
} catch (error) {
|
|
submitBtn.disabled = false;
|
|
buttonText.textContent = 'Import Performer';
|
|
buttonSpinner.style.display = 'none';
|
|
|
|
resultMessage.className = 'alert alert-danger mt-3';
|
|
resultMessage.innerHTML = `<strong>Error:</strong> ${error.message}`;
|
|
resultMessage.style.display = 'block';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
---
|
|
|
|
## Bulk Import with Progress
|
|
|
|
**File: `bulk-import.html`**
|
|
|
|
Import all performers with a real-time progress bar.
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Bulk Import - Goondex</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
|
|
<style>
|
|
.import-container {
|
|
max-width: 800px;
|
|
margin: 3rem auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.progress {
|
|
height: 30px;
|
|
}
|
|
|
|
.progress-bar {
|
|
font-size: 14px;
|
|
line-height: 30px;
|
|
}
|
|
|
|
.import-stats {
|
|
display: none;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.stat-box {
|
|
text-align: center;
|
|
padding: 1rem;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: #0d6efd;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<nav class="navbar navbar-dark bg-dark">
|
|
<div class="container-fluid">
|
|
<a class="navbar-brand" href="/">Goondex</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="import-container">
|
|
<h1 class="mb-4">Bulk Import Performers</h1>
|
|
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="text-muted">
|
|
Import all performers from your database. This will fetch full metadata from TPDB.
|
|
</p>
|
|
|
|
<!-- Start Button -->
|
|
<button id="start-btn" class="btn btn-primary btn-lg w-100 mb-4">
|
|
Start Import
|
|
</button>
|
|
|
|
<!-- Progress Bar (hidden initially) -->
|
|
<div id="progress-container" style="display: none;">
|
|
<div class="progress mb-2">
|
|
<div
|
|
id="progress-bar"
|
|
class="progress-bar progress-bar-striped progress-bar-animated"
|
|
role="progressbar"
|
|
style="width: 0%"
|
|
>
|
|
0%
|
|
</div>
|
|
</div>
|
|
|
|
<div id="status-text" class="alert alert-info">
|
|
Preparing to import...
|
|
</div>
|
|
|
|
<!-- Statistics (shown on completion) -->
|
|
<div id="import-stats" class="import-stats">
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<div class="stat-box">
|
|
<div id="stat-total" class="stat-number">0</div>
|
|
<div class="stat-label">Total</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stat-box">
|
|
<div id="stat-imported" class="stat-number">0</div>
|
|
<div class="stat-label">Imported</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stat-box">
|
|
<div id="stat-skipped" class="stat-number">0</div>
|
|
<div class="stat-label">Skipped</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stat-box">
|
|
<div id="stat-errors" class="stat-number">0</div>
|
|
<div class="stat-label">Errors</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = 'http://localhost:8080';
|
|
|
|
const startBtn = document.getElementById('start-btn');
|
|
const progressContainer = document.getElementById('progress-container');
|
|
const progressBar = document.getElementById('progress-bar');
|
|
const statusText = document.getElementById('status-text');
|
|
const importStats = document.getElementById('import-stats');
|
|
|
|
startBtn.addEventListener('click', function() {
|
|
// Hide button, show progress
|
|
startBtn.style.display = 'none';
|
|
progressContainer.style.display = 'block';
|
|
importStats.style.display = 'none';
|
|
|
|
// Create EventSource to listen for progress updates
|
|
const eventSource = new EventSource(
|
|
`${API_BASE}/api/import/all-performers/progress`
|
|
);
|
|
|
|
// When we receive a message
|
|
eventSource.onmessage = function(event) {
|
|
const update = JSON.parse(event.data);
|
|
|
|
// Check for error
|
|
if (update.error) {
|
|
statusText.className = 'alert alert-danger';
|
|
statusText.textContent = `Error: ${update.error}`;
|
|
progressBar.className = 'progress-bar bg-danger';
|
|
progressBar.style.width = '100%';
|
|
progressBar.textContent = 'Error';
|
|
startBtn.style.display = 'block';
|
|
eventSource.close();
|
|
return;
|
|
}
|
|
|
|
// Check for completion
|
|
if (update.complete) {
|
|
const result = update.result;
|
|
|
|
statusText.className = 'alert alert-success';
|
|
statusText.innerHTML = `
|
|
<strong>Import Complete!</strong><br>
|
|
Successfully imported ${result.imported} out of ${result.total} performers
|
|
`;
|
|
|
|
progressBar.className = 'progress-bar bg-success';
|
|
progressBar.style.width = '100%';
|
|
progressBar.textContent = '100%';
|
|
|
|
// Show statistics
|
|
document.getElementById('stat-total').textContent = result.total;
|
|
document.getElementById('stat-imported').textContent = result.imported;
|
|
document.getElementById('stat-skipped').textContent = result.skipped;
|
|
document.getElementById('stat-errors').textContent = result.errors;
|
|
importStats.style.display = 'block';
|
|
|
|
startBtn.textContent = 'Import Again';
|
|
startBtn.style.display = 'block';
|
|
|
|
eventSource.close();
|
|
return;
|
|
}
|
|
|
|
// Progress update
|
|
const percent = Math.round((update.current / update.total) * 100);
|
|
|
|
progressBar.style.width = `${percent}%`;
|
|
progressBar.textContent = `${percent}%`;
|
|
|
|
statusText.className = 'alert alert-info';
|
|
statusText.innerHTML = `
|
|
<strong>Importing:</strong> ${update.name}<br>
|
|
<small>Progress: ${update.current} / ${update.total}</small>
|
|
`;
|
|
};
|
|
|
|
// Handle connection errors
|
|
eventSource.onerror = function() {
|
|
statusText.className = 'alert alert-danger';
|
|
statusText.textContent = 'Connection error. Please try again.';
|
|
progressBar.className = 'progress-bar bg-danger';
|
|
startBtn.style.display = 'block';
|
|
eventSource.close();
|
|
};
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
---
|
|
|
|
## Simple Data Display
|
|
|
|
**File: `performers-list.html`**
|
|
|
|
Display a list of performers from the database.
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Performers - Goondex</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
|
|
<style>
|
|
.performer-grid {
|
|
max-width: 1400px;
|
|
margin: 2rem auto;
|
|
padding: 0 2rem;
|
|
}
|
|
|
|
.performer-card {
|
|
height: 100%;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.performer-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.performer-image {
|
|
width: 100%;
|
|
height: 350px;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 3rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<nav class="navbar navbar-dark bg-dark">
|
|
<div class="container-fluid">
|
|
<a class="navbar-brand" href="/">Goondex</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="performer-grid">
|
|
<h1 class="mb-4">All Performers</h1>
|
|
|
|
<!-- Loading indicator -->
|
|
<div id="loading" class="loading">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2">Loading performers...</p>
|
|
</div>
|
|
|
|
<!-- Performers grid -->
|
|
<div id="performers-grid" class="row row-cols-1 row-cols-md-3 row-cols-lg-5 g-4"></div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = 'http://localhost:8080';
|
|
|
|
// Load performers when page loads
|
|
window.addEventListener('load', loadPerformers);
|
|
|
|
async function loadPerformers() {
|
|
try {
|
|
// Fetch from search API with empty query (returns all)
|
|
const response = await fetch(`${API_BASE}/api/search?q=`);
|
|
const data = await response.json();
|
|
|
|
// Hide loading
|
|
document.getElementById('loading').style.display = 'none';
|
|
|
|
if (!data.success) {
|
|
alert('Error: ' + data.message);
|
|
return;
|
|
}
|
|
|
|
// Display performers
|
|
displayPerformers(data.data.performers || []);
|
|
|
|
} catch (error) {
|
|
document.getElementById('loading').style.display = 'none';
|
|
alert('Error loading performers: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function displayPerformers(performers) {
|
|
const grid = document.getElementById('performers-grid');
|
|
|
|
if (performers.length === 0) {
|
|
grid.innerHTML = `
|
|
<div class="col-12 text-center text-muted py-5">
|
|
<h3>No performers found</h3>
|
|
<p>Import some performers to get started</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
performers.forEach(performer => {
|
|
const imageUrl = performer.image_url || '/static/placeholder.jpg';
|
|
const flag = performer.nationality ? getFlagEmoji(performer.nationality) : '';
|
|
const age = calculateAge(performer.birthday);
|
|
|
|
html += `
|
|
<div class="col">
|
|
<div class="card performer-card">
|
|
<img
|
|
src="${imageUrl}"
|
|
class="performer-image"
|
|
alt="${performer.name}"
|
|
onerror="this.src='/static/placeholder.jpg'"
|
|
>
|
|
<div class="card-body">
|
|
<h5 class="card-title">${performer.name}</h5>
|
|
<p class="card-text">
|
|
${flag} ${performer.gender || ''}<br>
|
|
${age ? `Age: ${age}` : ''}
|
|
</p>
|
|
<a href="/performers/${performer.id}" class="btn btn-primary btn-sm w-100">
|
|
View Profile
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
grid.innerHTML = html;
|
|
}
|
|
|
|
// Helper: Convert country code to flag emoji
|
|
function getFlagEmoji(countryCode) {
|
|
const codePoints = countryCode
|
|
.toUpperCase()
|
|
.split('')
|
|
.map(char => 127397 + char.charCodeAt());
|
|
return String.fromCodePoint(...codePoints);
|
|
}
|
|
|
|
// Helper: Calculate age from birthday
|
|
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;
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
---
|
|
|
|
## Bootstrap Component Reference
|
|
|
|
Quick reference for Bootstrap 5 components to use in your pages.
|
|
|
|
### Buttons
|
|
|
|
```html
|
|
<!-- Primary button -->
|
|
<button class="btn btn-primary">Primary</button>
|
|
|
|
<!-- Success button -->
|
|
<button class="btn btn-success">Success</button>
|
|
|
|
<!-- Danger button -->
|
|
<button class="btn btn-danger">Danger</button>
|
|
|
|
<!-- Large button -->
|
|
<button class="btn btn-primary btn-lg">Large Button</button>
|
|
|
|
<!-- Small button -->
|
|
<button class="btn btn-primary btn-sm">Small Button</button>
|
|
|
|
<!-- Full width button -->
|
|
<button class="btn btn-primary w-100">Full Width</button>
|
|
|
|
<!-- Disabled button -->
|
|
<button class="btn btn-primary" disabled>Disabled</button>
|
|
```
|
|
|
|
### Alerts
|
|
|
|
```html
|
|
<!-- Success alert -->
|
|
<div class="alert alert-success">
|
|
<strong>Success!</strong> Your action was successful.
|
|
</div>
|
|
|
|
<!-- Error alert -->
|
|
<div class="alert alert-danger">
|
|
<strong>Error!</strong> Something went wrong.
|
|
</div>
|
|
|
|
<!-- Warning alert -->
|
|
<div class="alert alert-warning">
|
|
<strong>Warning!</strong> Please be careful.
|
|
</div>
|
|
|
|
<!-- Info alert -->
|
|
<div class="alert alert-info">
|
|
<strong>Info:</strong> Here's some information.
|
|
</div>
|
|
|
|
<!-- Dismissible alert -->
|
|
<div class="alert alert-success alert-dismissible fade show">
|
|
Success message
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
```
|
|
|
|
### Cards
|
|
|
|
```html
|
|
<!-- Basic card -->
|
|
<div class="card">
|
|
<img src="image.jpg" class="card-img-top" alt="...">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Card Title</h5>
|
|
<p class="card-text">Card description goes here.</p>
|
|
<a href="#" class="btn btn-primary">Go somewhere</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card with header -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
Featured
|
|
</div>
|
|
<div class="card-body">
|
|
<h5 class="card-title">Special title</h5>
|
|
<p class="card-text">Card content</p>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### Forms
|
|
|
|
```html
|
|
<!-- Basic form -->
|
|
<form>
|
|
<div class="mb-3">
|
|
<label for="input1" class="form-label">Email address</label>
|
|
<input type="email" class="form-control" id="input1">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="input2" class="form-label">Password</label>
|
|
<input type="password" class="form-control" id="input2">
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary">Submit</button>
|
|
</form>
|
|
|
|
<!-- Large input -->
|
|
<input type="text" class="form-control form-control-lg" placeholder="Large input">
|
|
|
|
<!-- Small input -->
|
|
<input type="text" class="form-control form-control-sm" placeholder="Small input">
|
|
|
|
<!-- Select dropdown -->
|
|
<select class="form-select">
|
|
<option selected>Choose...</option>
|
|
<option value="1">Option 1</option>
|
|
<option value="2">Option 2</option>
|
|
</select>
|
|
```
|
|
|
|
### Progress Bars
|
|
|
|
```html
|
|
<!-- Basic progress bar -->
|
|
<div class="progress">
|
|
<div class="progress-bar" style="width: 50%">50%</div>
|
|
</div>
|
|
|
|
<!-- Colored progress bars -->
|
|
<div class="progress">
|
|
<div class="progress-bar bg-success" style="width: 75%"></div>
|
|
</div>
|
|
|
|
<div class="progress">
|
|
<div class="progress-bar bg-danger" style="width: 100%"></div>
|
|
</div>
|
|
|
|
<!-- Striped and animated -->
|
|
<div class="progress">
|
|
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 75%"></div>
|
|
</div>
|
|
```
|
|
|
|
### Spinners
|
|
|
|
```html
|
|
<!-- Basic spinner -->
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
|
|
<!-- Colored spinners -->
|
|
<div class="spinner-border text-primary"></div>
|
|
<div class="spinner-border text-success"></div>
|
|
<div class="spinner-border text-danger"></div>
|
|
|
|
<!-- Small spinner -->
|
|
<div class="spinner-border spinner-border-sm"></div>
|
|
|
|
<!-- Growing spinner -->
|
|
<div class="spinner-grow" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
```
|
|
|
|
### Lists
|
|
|
|
```html
|
|
<!-- Basic list group -->
|
|
<ul class="list-group">
|
|
<li class="list-group-item">Item 1</li>
|
|
<li class="list-group-item">Item 2</li>
|
|
<li class="list-group-item">Item 3</li>
|
|
</ul>
|
|
|
|
<!-- Clickable list -->
|
|
<div class="list-group">
|
|
<a href="#" class="list-group-item list-group-item-action">
|
|
Clickable item
|
|
</a>
|
|
<a href="#" class="list-group-item list-group-item-action active">
|
|
Active item
|
|
</a>
|
|
</div>
|
|
```
|
|
|
|
### Grid System
|
|
|
|
```html
|
|
<!-- Responsive grid (1 column on mobile, 3 on tablet, 4 on desktop) -->
|
|
<div class="row row-cols-1 row-cols-md-3 row-cols-lg-4 g-4">
|
|
<div class="col">Column 1</div>
|
|
<div class="col">Column 2</div>
|
|
<div class="col">Column 3</div>
|
|
<div class="col">Column 4</div>
|
|
</div>
|
|
|
|
<!-- Two column layout -->
|
|
<div class="row">
|
|
<div class="col-md-6">Left column</div>
|
|
<div class="col-md-6">Right column</div>
|
|
</div>
|
|
|
|
<!-- Sidebar layout -->
|
|
<div class="row">
|
|
<div class="col-md-3">Sidebar</div>
|
|
<div class="col-md-9">Main content</div>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## CSS Tips
|
|
|
|
### Spacing Utilities
|
|
|
|
```html
|
|
<!-- Margin -->
|
|
<div class="m-3">Margin on all sides</div>
|
|
<div class="mt-3">Margin top</div>
|
|
<div class="mb-3">Margin bottom</div>
|
|
<div class="mx-3">Margin left and right</div>
|
|
<div class="my-3">Margin top and bottom</div>
|
|
|
|
<!-- Padding -->
|
|
<div class="p-3">Padding on all sides</div>
|
|
<div class="pt-3">Padding top</div>
|
|
<div class="pb-3">Padding bottom</div>
|
|
<div class="px-3">Padding left and right</div>
|
|
<div class="py-3">Padding top and bottom</div>
|
|
|
|
<!-- Sizes: 0, 1, 2, 3, 4, 5 (0 = 0, 5 = 3rem) -->
|
|
```
|
|
|
|
### Text Utilities
|
|
|
|
```html
|
|
<!-- Alignment -->
|
|
<div class="text-start">Left aligned</div>
|
|
<div class="text-center">Center aligned</div>
|
|
<div class="text-end">Right aligned</div>
|
|
|
|
<!-- Colors -->
|
|
<p class="text-primary">Primary text</p>
|
|
<p class="text-success">Success text</p>
|
|
<p class="text-danger">Danger text</p>
|
|
<p class="text-muted">Muted text</p>
|
|
|
|
<!-- Font weight -->
|
|
<p class="fw-bold">Bold text</p>
|
|
<p class="fw-normal">Normal text</p>
|
|
<p class="fw-light">Light text</p>
|
|
|
|
<!-- Font size -->
|
|
<p class="fs-1">Very large</p>
|
|
<p class="fs-5">Medium</p>
|
|
```
|
|
|
|
### Display Utilities
|
|
|
|
```html
|
|
<!-- Show/Hide -->
|
|
<div class="d-none">Hidden</div>
|
|
<div class="d-block">Shown as block</div>
|
|
<div class="d-inline-block">Inline block</div>
|
|
<div class="d-flex">Flexbox container</div>
|
|
|
|
<!-- Responsive display -->
|
|
<div class="d-none d-md-block">Hidden on mobile, shown on tablet+</div>
|
|
```
|
|
|
|
---
|
|
|
|
## Quick JavaScript Snippets
|
|
|
|
### Show/Hide Elements
|
|
|
|
```javascript
|
|
// Hide element
|
|
document.getElementById('myElement').style.display = 'none';
|
|
|
|
// Show element
|
|
document.getElementById('myElement').style.display = 'block';
|
|
|
|
// Toggle visibility
|
|
const el = document.getElementById('myElement');
|
|
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
|
```
|
|
|
|
### Change Text Content
|
|
|
|
```javascript
|
|
document.getElementById('myElement').textContent = 'New text';
|
|
document.getElementById('myElement').innerHTML = '<strong>HTML</strong> text';
|
|
```
|
|
|
|
### Add/Remove CSS Classes
|
|
|
|
```javascript
|
|
// Add class
|
|
document.getElementById('myElement').classList.add('active');
|
|
|
|
// Remove class
|
|
document.getElementById('myElement').classList.remove('active');
|
|
|
|
// Toggle class
|
|
document.getElementById('myElement').classList.toggle('active');
|
|
```
|
|
|
|
### Get Form Values
|
|
|
|
```javascript
|
|
const inputValue = document.getElementById('myInput').value;
|
|
const selectValue = document.getElementById('mySelect').value;
|
|
```
|
|
|
|
---
|
|
|
|
## Need Help?
|
|
|
|
All the JavaScript in these templates is ready to use - just copy and paste! If you need to customize something:
|
|
|
|
1. Look for comments in the JavaScript (`// like this`)
|
|
2. The `API_BASE` constant at the top controls the server URL
|
|
3. Use AI assistance to modify the JavaScript parts
|
|
4. Focus on the HTML/CSS which you're already great at!
|
|
|
|
The templates are fully functional and follow Bootstrap best practices.
|