- Add LibraryData type to templates/types.go - Update bookshelf template to accept libraries parameter - Render libraries server-side for faster initial page load - Libraries now populated from server data instead of AJAX fetch - JavaScript still uses API for dynamic content (bookshelf items) - Update /bookshelf route to fetch libraries server-side before render - Properly handle UUID and pgtype.Text conversions - Maintain API endpoint compatibility for JavaScript calls This improves initial page load performance while preserving dynamic functionality via API calls.
268 lines
12 KiB
Templ
268 lines
12 KiB
Templ
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/header.js"></script>
|
|
<script src="/static/search.js"></script>
|
|
<link href="/static/style.css" rel="stylesheet">
|
|
</head>
|
|
<body class="theme-tokyo-night">
|
|
@Header(user, "/bookshelf")
|
|
<div class="max-w-7xl mx-auto 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>
|
|
}
|