Goondex/internal/web/static/js/app.js
Stu Leak 16fb407a3c v0.1.0-dev4: Add web frontend with UI component library
- 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>
2025-11-17 10:47:30 -05:00

478 lines
15 KiB
JavaScript

// Goondex Web UI JavaScript
// Modal handling
function openModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('active');
}
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('active');
}
}
// Import functions
// Global Search
let searchTimeout;
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('global-search');
if (searchInput) {
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (query.length < 2) {
document.getElementById('global-search-results').style.display = 'none';
return;
}
searchTimeout = setTimeout(() => globalSearch(query), 300);
});
}
});
async function globalSearch(query) {
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const result = await response.json();
if (result.success) {
displayGlobalSearchResults(result.data);
}
} catch (error) {
console.error('Search failed:', error);
}
}
function displayGlobalSearchResults(data) {
const resultsDiv = document.getElementById('global-search-results');
let html = '<div style="background: var(--color-bg-elevated); padding: 1.5rem; border-radius: 0.5rem; border: 1px solid var(--color-border);">';
if (data.total === 0) {
html += '<p style="color: var(--color-text-secondary);">No results found</p>';
} else {
html += `<p style="color: var(--color-text-secondary); margin-bottom: 1rem;">Found ${data.total} results</p>`;
if (data.performers && data.performers.length > 0) {
html += '<h4 style="color: var(--color-brand); margin-top: 0;">Performers</h4><ul class="item-list">';
data.performers.slice(0, 5).forEach(p => {
html += `<li><a href="/performers/${p.id}">${p.name}</a></li>`;
});
html += '</ul>';
}
if (data.studios && data.studios.length > 0) {
html += '<h4 style="color: var(--color-brand);">Studios</h4><ul class="item-list">';
data.studios.slice(0, 5).forEach(s => {
html += `<li><a href="/studios/${s.id}">${s.name}</a></li>`;
});
html += '</ul>';
}
if (data.scenes && data.scenes.length > 0) {
html += '<h4 style="color: var(--color-brand);">Scenes</h4><ul class="item-list">';
data.scenes.slice(0, 5).forEach(sc => {
html += `<li><a href="/scenes/${sc.id}">${sc.title}</a></li>`;
});
html += '</ul>';
}
if (data.tags && data.tags.length > 0) {
html += '<h4 style="color: var(--color-brand);">Tags</h4><div class="tag-list">';
data.tags.slice(0, 10).forEach(t => {
html += `<span class="tag">${t.name}</span>`;
});
html += '</div>';
}
}
html += '</div>';
resultsDiv.innerHTML = html;
resultsDiv.style.display = 'block';
}
// Bulk Import Functions
async function bulkImportAll() {
if (!confirm('This will import ALL data from TPDB. This may take several hours. Continue?')) {
return;
}
setImportStatus('import-all', 'Importing all data from TPDB... This may take a while.', false);
try {
const response = await fetch('/api/import/all', {
method: 'POST'
});
const result = await response.json();
if (result.success) {
let message = result.message + '\n\n';
if (result.data) {
result.data.forEach(r => {
message += `${r.EntityType}: ${r.Imported}/${r.Total} imported, ${r.Failed} failed\n`;
});
}
setImportStatus('import-all', message, true);
setTimeout(() => {
closeModal('import-all-modal');
location.reload();
}, 3000);
} else {
setImportStatus('import-all', result.message, false);
}
} catch (error) {
setImportStatus('import-all', 'Error: ' + error.message, false);
}
}
async function bulkImportPerformers() {
if (!confirm('This will import ALL performers from TPDB. Continue?')) {
return;
}
// Show progress modal
showProgressModal('performers');
// Connect to SSE endpoint
const eventSource = new EventSource('/api/import/all-performers/progress');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.error) {
updateProgress('performers', 0, 0, data.error, true);
eventSource.close();
return;
}
if (data.complete) {
updateProgress('performers', 100, 100, `Complete! Imported ${data.result.Imported}/${data.result.Total} performers`, false);
eventSource.close();
setTimeout(() => {
closeProgressModal();
location.reload();
}, 2000);
} else {
updateProgress('performers', data.current, data.total, data.message, false);
}
};
eventSource.onerror = function() {
updateProgress('performers', 0, 0, 'Connection error', true);
eventSource.close();
};
}
async function bulkImportStudios() {
if (!confirm('This will import ALL studios from TPDB. Continue?')) {
return;
}
// Show progress modal
showProgressModal('studios');
// Connect to SSE endpoint
const eventSource = new EventSource('/api/import/all-studios/progress');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.error) {
updateProgress('studios', 0, 0, data.error, true);
eventSource.close();
return;
}
if (data.complete) {
updateProgress('studios', 100, 100, `Complete! Imported ${data.result.Imported}/${data.result.Total} studios`, false);
eventSource.close();
setTimeout(() => {
closeProgressModal();
location.reload();
}, 2000);
} else {
updateProgress('studios', data.current, data.total, data.message, false);
}
};
eventSource.onerror = function() {
updateProgress('studios', 0, 0, 'Connection error', true);
eventSource.close();
};
}
async function bulkImportScenes() {
if (!confirm('This will import ALL scenes from TPDB. Continue?')) {
return;
}
// Show progress modal
showProgressModal('scenes');
// Connect to SSE endpoint
const eventSource = new EventSource('/api/import/all-scenes/progress');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.error) {
updateProgress('scenes', 0, 0, data.error, true);
eventSource.close();
return;
}
if (data.complete) {
updateProgress('scenes', 100, 100, `Complete! Imported ${data.result.Imported}/${data.result.Total} scenes`, false);
eventSource.close();
setTimeout(() => {
closeProgressModal();
location.reload();
}, 2000);
} else {
updateProgress('scenes', data.current, data.total, data.message, false);
}
};
eventSource.onerror = function() {
updateProgress('scenes', 0, 0, 'Connection error', true);
eventSource.close();
};
}
function toggleFilterbar() {
document.getElementById('filterbar').classList.toggle('open');
}
function applyFilters() {
// Hook for your search/filter logic
console.log("Applying filters…");
}
function sortTable(columnIndex) {
const table = document.querySelector(".gx-table tbody");
const rows = Array.from(table.querySelectorAll("tr"));
const asc = table.getAttribute("data-sort") !== "asc";
table.setAttribute("data-sort", asc ? "asc" : "desc");
rows.sort((a, b) => {
const A = a.children[columnIndex].innerText.trim().toLowerCase();
const B = b.children[columnIndex].innerText.trim().toLowerCase();
if (!isNaN(A) && !isNaN(B)) return asc ? A - B : B - A;
return asc ? A.localeCompare(B) : B.localeCompare(A);
});
rows.forEach(r => table.appendChild(r));
}
// Search-based Import Functions
async function importPerformer() {
const query = document.getElementById('performer-query').value;
if (!query) {
alert('Please enter a performer name');
return;
}
setImportStatus('performer', 'Searching...', false);
try {
const response = await fetch('/api/import/performer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
const result = await response.json();
if (result.success) {
setImportStatus('performer', result.message, true);
setTimeout(() => {
closeModal('search-performer-modal');
location.reload();
}, 1500);
} else {
setImportStatus('performer', result.message, false);
}
} catch (error) {
setImportStatus('performer', 'Error: ' + error.message, false);
}
}
async function importStudio() {
const query = document.getElementById('studio-query').value;
if (!query) {
alert('Please enter a studio name');
return;
}
setImportStatus('studio', 'Searching...', false);
try {
const response = await fetch('/api/import/studio', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
const result = await response.json();
if (result.success) {
setImportStatus('studio', result.message, true);
setTimeout(() => {
closeModal('search-studio-modal');
location.reload();
}, 1500);
} else {
setImportStatus('studio', result.message, false);
}
} catch (error) {
setImportStatus('studio', 'Error: ' + error.message, false);
}
}
async function importScene() {
const query = document.getElementById('scene-query').value;
if (!query) {
alert('Please enter a scene title');
return;
}
setImportStatus('scene', 'Searching...', false);
try {
const response = await fetch('/api/import/scene', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
const result = await response.json();
if (result.success) {
setImportStatus('scene', result.message, true);
setTimeout(() => {
closeModal('search-scene-modal');
location.reload();
}, 1500);
} else {
setImportStatus('scene', result.message, false);
}
} catch (error) {
setImportStatus('scene', 'Error: ' + error.message, false);
}
}
async function syncAll() {
const force = document.getElementById('sync-force').checked;
setImportStatus('sync', 'Syncing all data from TPDB...', false);
try {
const response = await fetch('/api/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ force })
});
const result = await response.json();
if (result.success) {
let message = result.message + '\n\n';
if (result.data) {
result.data.forEach(r => {
message += `${r.EntityType}: ${r.Updated} updated, ${r.Failed} failed\n`;
});
}
setImportStatus('sync', message, true);
setTimeout(() => {
closeModal('sync-modal');
location.reload();
}, 2000);
} else {
setImportStatus('sync', result.message, false);
}
} catch (error) {
setImportStatus('sync', 'Error: ' + error.message, false);
}
}
function setImportStatus(type, message, success) {
const statusEl = document.getElementById(`${type}-import-status`);
if (statusEl) {
statusEl.textContent = message;
statusEl.className = 'result-message ' + (success ? 'success' : 'error');
statusEl.style.display = 'block';
}
}
// Close modals when clicking outside
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.classList.remove('active');
}
}
// Image error handling
function handleImageError(img) {
img.style.display = 'none';
}
// Progress Modal Functions
function showProgressModal(entityType) {
const modal = document.getElementById('progress-modal');
if (!modal) {
// Create progress modal if it doesn't exist
const modalHTML = `
<div id="progress-modal" class="modal active">
<div class="modal-content">
<div class="modal-header">
<h3>Import Progress</h3>
</div>
<div id="progress-container">
<div class="progress-bar-wrapper">
<div class="progress-bar" id="progress-bar">
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
</div>
<div class="progress-text" id="progress-text">Starting...</div>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
} else {
modal.classList.add('active');
}
}
function closeProgressModal() {
const modal = document.getElementById('progress-modal');
if (modal) {
modal.classList.remove('active');
}
}
function updateProgress(entityType, current, total, message, isError) {
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
if (progressFill && progressText) {
const percent = total > 0 ? (current / total * 100) : 0;
progressFill.style.width = percent + '%';
progressText.textContent = message;
if (isError) {
progressFill.style.background = 'var(--color-warning)';
progressText.style.color = 'var(--color-warning)';
} else {
progressFill.style.background = 'linear-gradient(135deg, var(--color-brand) 0%, var(--color-keypoint) 100%)';
progressText.style.color = 'var(--color-text-primary)';
}
}
}