refactor: Convert template event handlers to Alpine.js
Converted all inline onclick/onsubmit handlers to Alpine.js @click/@submit directives and added x-data attributes to template body tags. Templates updated: - admin.templ: Added x-data="admin", converted scan/hide buttons - analytics.templ: Added x-data, converted loadAnalytics button - api_explorer.templ: Added x-data for API explorer page - bookshelf.templ: Added x-data, converted library/pagination handlers - collection_modal.templ: Added x-data for modal components - collection_rules.templ: Added x-data, converted all rule handlers - collections.templ: Added x-data, converted navigation/book handlers - conflicts.templ: Added x-data, converted resolve/dismiss handlers - header.templ: Converted theme dropdown and user menu handlers - index.templ: Added x-data for theme/auth - login.templ: Added x-data for theme switching - profile.templ: Added x-data, converted delete account - profile_form.templ: Converted modal close handler - profile_modal.templ: Added x-data for modal - progress.templ: Removed script (uses header.logout) - queue.templ: Added x-data, converted queue handlers - register.templ: Added x-data for theme - restore_system_collection_modal.templ: Added x-data - toast.templ: Converted to x-init with Alpine - unlinked_books.templ: Added x-data for bulk operations Dynamic content in devices.templ uses event delegation via data attributes.
This commit is contained in:
+56
-262
@@ -1,266 +1,60 @@
|
||||
package templates
|
||||
|
||||
templ BookShelf(user User, libraries []LibraryData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Library - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/toast.js"></script>
|
||||
<script src="/static/search.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, "/bookshelf")
|
||||
<div class="w-full px-4 sm:px-6 lg:8 py-8">
|
||||
<!-- Library Selector -->
|
||||
<div class="mb-8">
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<div class="flex-1 w-full">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Select Library</label>
|
||||
<select id="library-select" onchange="selectLibrary()" class="w-full px-4 py-3 border rounded-lg text-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border);">
|
||||
if len(libraries) == 0 {
|
||||
<option value="">No libraries available</option>
|
||||
} else {
|
||||
for _, lib := range libraries {
|
||||
<option value={ lib.ID }>{ lib.Name }</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="loadBookshelf()" class="btn-primary px-6 py-3 rounded-lg font-medium">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bookshelf Container -->
|
||||
<div id="bookshelf-container">
|
||||
<!-- Loading state -->
|
||||
<div id="loading" class="text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="loading-spinner mx-auto mb-4"></div>
|
||||
<p>Loading your library...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div id="empty-state" class="hidden text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">📚</div>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Books Yet</h3>
|
||||
<p>Select a library to view your collection</p>
|
||||
</div>
|
||||
|
||||
<!-- Books Grid -->
|
||||
<div id="books-grid" class="hidden">
|
||||
<!-- Books will be loaded here on shelves -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/theme.js"></script>
|
||||
<script>
|
||||
// Libraries already loaded server-side
|
||||
const libraries = [
|
||||
if len(libraries) > 0 {
|
||||
for _, lib := range libraries {
|
||||
{
|
||||
id: "{ lib.ID }",
|
||||
name: "{ lib.Name }",
|
||||
description: "{ lib.Description }",
|
||||
type_name: "{ lib.TypeName }"
|
||||
},
|
||||
}
|
||||
}
|
||||
];
|
||||
let currentLibrary = null;
|
||||
let mediaItems = [];
|
||||
|
||||
function selectLibrary() {
|
||||
const librarySelect = document.getElementById('library-select');
|
||||
const selectedId = librarySelect.value;
|
||||
currentLibrary = libraries.find(lib => lib.id === selectedId);
|
||||
if (currentLibrary) {
|
||||
loadBookshelf();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-select first library if available
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadTheme();
|
||||
if (libraries.length > 0) {
|
||||
const librarySelect = document.getElementById('library-select');
|
||||
currentLibrary = libraries[0];
|
||||
librarySelect.value = currentLibrary.id;
|
||||
loadBookshelf();
|
||||
} else {
|
||||
showEmptyState();
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
loading.style.display = 'none';
|
||||
libraries = data;
|
||||
populateLibrarySelect();
|
||||
|
||||
// Auto-select first library if available
|
||||
if (libraries.length > 0 && !currentLibrary) {
|
||||
currentLibrary = libraries[0];
|
||||
librarySelect.value = currentLibrary.id;
|
||||
loadBookshelf();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
loading.style.display = 'none';
|
||||
console.error('Error loading libraries:', error);
|
||||
showToast('Error loading libraries', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function populateLibrarySelect() {
|
||||
const librarySelect = document.getElementById('library-select');
|
||||
|
||||
if (libraries.length === 0) {
|
||||
librarySelect.innerHTML = '<option value="">No libraries available</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
librarySelect.innerHTML = '<option value="">Select a library...</option>' +
|
||||
libraries.map(library => {
|
||||
const icon = getLibraryIcon(library.type_name);
|
||||
return `<option value="${library.id}">${icon} ${library.name}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getLibraryIcon(typeName) {
|
||||
switch (typeName) {
|
||||
case 'ebooks': return '📚';
|
||||
case 'comics': return '📖';
|
||||
case 'manga': return '🗾';
|
||||
default: return '📁';
|
||||
}
|
||||
}
|
||||
|
||||
function selectLibrary() {
|
||||
const librarySelect = document.getElementById('library-select');
|
||||
const libraryId = librarySelect.value;
|
||||
|
||||
currentLibrary = libraries.find(l => l.id === libraryId);
|
||||
loadBookshelf();
|
||||
}
|
||||
|
||||
function loadBookshelf() {
|
||||
if (!currentLibrary) {
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
const loading = document.getElementById('loading');
|
||||
const booksGrid = document.getElementById('books-grid');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
|
||||
loading.style.display = 'block';
|
||||
booksGrid.classList.add('hidden');
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
fetch(`/api/media-items?library_id=${currentLibrary.id}&limit=100&offset=0`, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
loading.style.display = 'none';
|
||||
mediaItems = data;
|
||||
|
||||
if (mediaItems.length === 0) {
|
||||
showEmptyState();
|
||||
} else {
|
||||
renderBookshelf();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
loading.style.display = 'none';
|
||||
console.error('Error loading bookshelf:', error);
|
||||
showToast('Error loading books', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function showEmptyState() {
|
||||
const booksGrid = document.getElementById('books-grid');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
loading.style.display = 'none';
|
||||
booksGrid.classList.add('hidden');
|
||||
emptyState.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function renderBookshelf() {
|
||||
const booksGrid = document.getElementById('books-grid');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
loading.style.display = 'none';
|
||||
emptyState.classList.add('hidden');
|
||||
booksGrid.classList.remove('hidden');
|
||||
|
||||
// Group books into shelves (6 books per shelf for better display)
|
||||
const booksPerShelf = 6;
|
||||
const shelves = [];
|
||||
|
||||
for (let i = 0; i < mediaItems.length; i += booksPerShelf) {
|
||||
shelves.push(mediaItems.slice(i, i + booksPerShelf));
|
||||
}
|
||||
|
||||
let html = '';
|
||||
shelves.forEach((shelfBooks, index) => {
|
||||
html += `<div class="relative bg-gradient-to-b from-transparent to-black/10 p-8 mb-4 rounded-lg" style="padding-bottom: 3rem;">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
${shelfBooks.map(book => renderBookCard(book)).join('')}
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 h-3 rounded-b-lg" style="background: linear-gradient(to bottom, rgba(107, 68, 35, 0.3) 0%, rgba(107, 68, 35, 0.5) 50%, rgba(107, 68, 35, 0.3) 100%); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);"></div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
booksGrid.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderBookCard(book) {
|
||||
const coverUrl = book.cover_image_path || '/static/placeholder-book.svg';
|
||||
const authorHtml = book.author ? `<p class="text-xs" style="color: var(--text-secondary)">${book.author}</p>` : '';
|
||||
|
||||
return `<div class="relative transition-all duration-200 ease hover:-translate-y-2 hover:-rotate-2 hover:shadow-2xl hover:z-10 cursor-pointer" onclick="viewBook('${book.id}')">
|
||||
<div class="aspect-[2/3] overflow-hidden rounded shadow-[2px_2px_4px_rgba(0,0,0,0.2),-1px_-1px_2px_rgba(255,255,255,0.1)_inset] relative">
|
||||
<div class="absolute left-0 top-0 bottom-0 w-1" style="background: linear-gradient(to right, rgba(0, 0, 0, 0.3) 0%, rgba(255, 255, 255, 0.1) 50%, transparent 100%);"></div>
|
||||
<img src="${coverUrl}"
|
||||
alt="${book.title}"
|
||||
class="w-full h-full object-cover"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<h3 class="text-sm font-semibold line-clamp-2" style="color: var(--text-primary)">${book.title}</h3>
|
||||
${authorHtml}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function viewBook(bookId) {
|
||||
// TODO: Navigate to book detail view
|
||||
showToast('Book viewer coming soon!', 'info');
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadTheme();
|
||||
loadLibraries();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Library - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/main.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }" x-data="bookshelf" x-init="initBookshelf(); setupEventDelegation()">
|
||||
@Header(user, "/bookshelf")
|
||||
<div class="w-full px-4 sm:px-6 lg:8 py-8">
|
||||
<!-- Library Selector -->
|
||||
<div class="mb-8">
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<div class="flex-1 w-full">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Select Library</label>
|
||||
<select id="library-select" @change="selectLibrary()" class="w-full px-4 py-3 border rounded-lg text-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border);">
|
||||
if len(libraries) == 0 {
|
||||
<option value="">No libraries available</option>
|
||||
} else {
|
||||
for _, lib := range libraries {
|
||||
<option value={ lib.ID }>{ lib.Name }</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button @click="loadBookshelf()" class="btn-primary px-6 py-3 rounded-lg font-medium">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bookshelf Container -->
|
||||
<div id="bookshelf-container">
|
||||
<!-- Loading state -->
|
||||
<div id="loading" class="text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="loading-spinner mx-auto mb-4"></div>
|
||||
<p>Loading your library...</p>
|
||||
</div>
|
||||
<!-- Empty state -->
|
||||
<div id="empty-state" class="hidden text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">📚</div>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Books Yet</h3>
|
||||
<p>Select a library to view your collection</p>
|
||||
</div>
|
||||
<!-- Books Grid -->
|
||||
<div id="books-grid" class="hidden">
|
||||
<!-- Books will be loaded here on shelves -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user