- Move header.js script from individual templates to header.templ - Removes duplicate script tags from bookshelf, collections, progress, etc. - Fixes indentation in docs.templ
597 lines
28 KiB
Templ
597 lines
28 KiB
Templ
package templates
|
||
|
||
import "bookhoard/internal/handlers"
|
||
|
||
templ Collection(user User, collections []CollectionData, errorMessage string) {
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Collections - Bookhoard</title>
|
||
<script src="/static/htmx.min.js"></script>
|
||
<script src="/static/toast.js"></script>
|
||
<link href="/static/style.css" rel="stylesheet">
|
||
</head>
|
||
<body class="theme-{ user.Theme }">
|
||
@Header(user, "/collections")
|
||
|
||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
<div class="mb-8 flex justify-between items-center">
|
||
<div>
|
||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">My Collections</h1>
|
||
<p style="color: var(--text-secondary)">Organize your books into custom collections</p>
|
||
</div>
|
||
<button onclick="showCreateModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||
➕ New Collection
|
||
</button>
|
||
</div>
|
||
|
||
<div id="collections-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
if len(collections) == 0 {
|
||
<div class="text-center py-16 col-span-full" 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 Collections Yet</h3>
|
||
<p class="mb-4">Create collections to organize your books</p>
|
||
<button onclick="showCreateModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||
Create Your First Collection
|
||
</button>
|
||
</div>
|
||
}
|
||
|
||
for _, col := range collections {
|
||
<div class="card p-6 rounded-lg border cursor-pointer hover:shadow-lg transition-shadow"
|
||
style="background-color: var(--bg-secondary); border-color: { col.Color }; border-left-width: 4px; border-left-style: solid;"
|
||
onclick="viewCollection('{ col.ID }')">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div class="text-3xl">{ col.Icon }</div>
|
||
<div class="flex space-x-2">
|
||
<button onclick="event.stopPropagation(); editCollection('{ col.ID }')"
|
||
class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
||
✏️
|
||
</button>
|
||
<button onclick="event.stopPropagation(); deleteCollection('{ col.ID }')"
|
||
class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ col.Name }</h3>
|
||
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ col.Description }</p>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<div id="create-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Collection</h2>
|
||
<button onclick="hideCreateModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||
</div>
|
||
|
||
<form id="create-form" onsubmit="handleCreate(event)">
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name</label>
|
||
<input type="text" id="collection-name" required
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
placeholder="My Reading List">
|
||
</div>
|
||
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label>
|
||
<textarea id="collection-description"
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
placeholder="Optional description"
|
||
rows="3"></textarea>
|
||
</div>
|
||
|
||
<div class="mb-6">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Color</label>
|
||
<div class="flex gap-2">
|
||
<button type="button" onclick="selectColor('#7aa2f7')" class="w-8 h-8 rounded-full color-option" style="background-color: #7aa2f7;"></button>
|
||
<button type="button" onclick="selectColor('#f7768e')" class="w-8 h-8 rounded-full color-option" style="background-color: #f7768e;"></button>
|
||
<button type="button" onclick="selectColor('#e0af68')" class="w-8 h-8 rounded-full color-option" style="background-color: #e0af68;"></button>
|
||
<button type="button" onclick="selectColor('#9ece6a')" class="w-8 h-8 rounded-full color-option" style="background-color: #9ece6a;"></button>
|
||
<button type="button" onclick="selectColor('#7dcfff')" class="w-8 h-8 rounded-full color-option" style="background-color: #7dcfff;"></button>
|
||
<button type="button" onclick="selectColor('#bb9af7')" class="w-8 h-8 rounded-full color-option" style="background-color: #bb9af7;"></button>
|
||
</div>
|
||
<input type="hidden" id="collection-color" value="#7aa2f7">
|
||
</div>
|
||
|
||
<div class="flex justify-end space-x-3">
|
||
<button type="button" onclick="hideCreateModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||
Cancel
|
||
</button>
|
||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||
Create Collection
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let selectedColor = '#7aa2f7';
|
||
|
||
function showCreateModal() {
|
||
document.getElementById('create-modal').classList.remove('hidden');
|
||
}
|
||
|
||
function hideCreateModal() {
|
||
document.getElementById('create-modal').classList.add('hidden');
|
||
document.getElementById('create-form').reset();
|
||
selectedColor = '#7aa2f7';
|
||
updateColorSelection();
|
||
}
|
||
|
||
function selectColor(color) {
|
||
selectedColor = color;
|
||
document.getElementById('collection-color').value = color;
|
||
updateColorSelection();
|
||
}
|
||
|
||
function updateColorSelection() {
|
||
document.querySelectorAll('.color-option').forEach(btn => {
|
||
btn.style.outline = 'none';
|
||
btn.style.outlineOffset = '0';
|
||
});
|
||
const selectedBtn = document.querySelector(`.color-option[onclick="selectColor('${selectedColor}')"]`);
|
||
if (selectedBtn) {
|
||
selectedBtn.style.outline = '3px solid var(--text-primary)';
|
||
selectedBtn.style.outlineOffset = '3px';
|
||
}
|
||
}
|
||
|
||
function handleCreate(event) {
|
||
event.preventDefault();
|
||
|
||
const data = {
|
||
name: document.getElementById('collection-name').value,
|
||
description: document.getElementById('collection-description').value,
|
||
color: selectedColor,
|
||
icon: '📚'
|
||
};
|
||
|
||
fetch('/api/collections', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||
},
|
||
body: JSON.stringify(data)
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
showToast('Collection created successfully', 'success');
|
||
hideCreateModal();
|
||
location.reload();
|
||
})
|
||
.catch(error => {
|
||
showToast('Failed to create collection', 'error');
|
||
});
|
||
}
|
||
|
||
function viewCollection(id) {
|
||
window.location.href = `/collections/${id}`;
|
||
}
|
||
|
||
function editCollection(id) {
|
||
showToast('Edit feature coming soon!', 'info');
|
||
}
|
||
|
||
function deleteCollection(id) {
|
||
if (!confirm('Are you sure you want to delete this collection?')) return;
|
||
|
||
fetch(`/api/collections/${id}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||
})
|
||
.then(response => {
|
||
if (response.ok) {
|
||
showToast('Collection deleted successfully', 'success');
|
||
location.reload();
|
||
} else {
|
||
showToast('Failed to delete collection', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showToast('Failed to delete collection', 'error');
|
||
});
|
||
}
|
||
|
||
function logout() {
|
||
localStorage.removeItem('token');
|
||
window.location.href = '/login';
|
||
}
|
||
</script>
|
||
|
||
@ErrorToast(errorMessage)
|
||
</body>
|
||
</html>
|
||
}
|
||
|
||
templ CollectionDetail(user User, collection CollectionData, books []handlers.BookInfo) {
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{ collection.Name } - Bookhoard</title>
|
||
<script src="/static/htmx.min.js"></script>
|
||
<script src="/static/toast.js"></script>
|
||
<link href="/static/style.css" rel="stylesheet">
|
||
</head>
|
||
<body class="theme-{ user.Theme }">
|
||
@Header(user, "/collections")
|
||
|
||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
<div class="mb-6">
|
||
<button onclick="backToCollections()" class="btn-secondary px-4 py-2 rounded-lg mb-4">
|
||
← Back to Collections
|
||
</button>
|
||
<div class="flex items-center gap-4">
|
||
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div>
|
||
<div>
|
||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1>
|
||
<p style="color: var(--text-secondary)">{ collection.Description }</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-6 flex justify-between items-center">
|
||
<div class="flex items-center gap-4">
|
||
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Books in this Collection</h2>
|
||
<span id="selected-count" class="hidden px-3 py-1 text-sm rounded" style="background-color: var(--accent); color: var(--bg-primary);">
|
||
0 selected
|
||
</span>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<div class="flex-1 max-w-md">
|
||
<input type="text" id="collection-search" placeholder="Search within collection..."
|
||
onkeyup="filterCollectionBooks()"
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
||
</div>
|
||
<button id="bulk-remove-btn" onclick="removeSelectedBooks()" disabled
|
||
class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed">
|
||
🗑️ Remove Selected
|
||
</button>
|
||
<button onclick="showAddBooksModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||
➕ Add Books
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="books-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||
if len(books) == 0 {
|
||
<div id="empty-state" class="col-span-full text-center py-16" style="color: var(--text-secondary)">No books in this collection yet.</div>
|
||
}
|
||
|
||
for _, book := range books {
|
||
<div class="card p-4 rounded-lg border hover:shadow-lg transition-shadow"
|
||
style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||
<div class="flex items-start gap-4">
|
||
<input type="checkbox"
|
||
onchange="toggleBookForRemoval('{ book.MediaItemID }')"
|
||
class="w-5 h-5 mt-2">
|
||
<div class="aspect-w-3 aspect-h-4 flex-shrink-0 w-24 mb-3 overflow-hidden rounded">
|
||
<img src="{ book.CoverImagePath }" alt="Cover"
|
||
class="w-full h-32 object-cover rounded"
|
||
onerror="this.src='/static/placeholder-book.svg'">
|
||
</div>
|
||
<div class="flex-1">
|
||
<h3 class="font-semibold text-lg mb-1 line-clamp-2" style="color: var(--text-primary)">{ book.Title }</h3>
|
||
if book.Author != "" {
|
||
<p class="text-sm" style="color: var(--text-secondary)">by { book.Author }</p>
|
||
}
|
||
<button onclick="removeBook('{ book.MediaItemID }')"
|
||
class="mt-2 px-3 py-1 text-sm border rounded hover:opacity-80"
|
||
style="border-color: var(--border); color: var(--text-secondary);">
|
||
Remove
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<div id="add-books-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||
<div class="card rounded-lg p-6 w-full max-w-2xl mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Books to Collection</h2>
|
||
<button onclick="hideAddBooksModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||
</div>
|
||
<p class="mb-4" style="color: var(--text-secondary)">Search and select books to add to this collection.</p>
|
||
<div class="mb-4">
|
||
<input type="text" id="book-search" placeholder="Search books..."
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
||
</div>
|
||
<div id="book-results" class="max-h-64 overflow-y-auto mb-4"></div>
|
||
<div class="flex justify-end space-x-3">
|
||
<button type="button" onclick="hideAddBooksModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||
Cancel
|
||
</button>
|
||
<button type="button" onclick="addSelectedBooks()" class="btn-primary px-4 py-2 rounded-lg">
|
||
Add Selected Books
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let collectionId = '{ collection.ID }';
|
||
let selectedBooks = new Set();
|
||
let booksToRemove = new Set();
|
||
let allBooks = [
|
||
{ range books }{
|
||
{
|
||
id: '{ book.MediaItemID }',
|
||
title: '{ book.Title }',
|
||
author: '{ book.Author }',
|
||
cover: '{ book.CoverImagePath }'
|
||
},
|
||
}{ end }
|
||
];
|
||
let ws = null;
|
||
|
||
function backToCollections() {
|
||
window.location.href = '/collections';
|
||
}
|
||
|
||
function connectWebSocket() {
|
||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
const wsUrl = `${protocol}//${window.location.host}/ws/sync`;
|
||
|
||
ws = new WebSocket(wsUrl);
|
||
|
||
ws.onopen = function() {
|
||
console.log('WebSocket connected');
|
||
};
|
||
|
||
ws.onmessage = function(event) {
|
||
const message = JSON.parse(event.data);
|
||
|
||
if (message.type === 'collection_updated' && message.data.collection_id === collectionId) {
|
||
showToast(`Collection updated: ${message.data.action} (${message.data.count} books)`, 'info');
|
||
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 1000);
|
||
}
|
||
};
|
||
|
||
ws.onclose = function() {
|
||
console.log('WebSocket disconnected, reconnecting in 5s...');
|
||
setTimeout(connectWebSocket, 5000);
|
||
};
|
||
|
||
ws.onerror = function(error) {
|
||
console.error('WebSocket error:', error);
|
||
};
|
||
}
|
||
|
||
connectWebSocket();
|
||
|
||
function filterCollectionBooks() {
|
||
const searchTerm = document.getElementById('collection-search').value.toLowerCase();
|
||
const booksContainer = document.getElementById('books-container');
|
||
const bookCards = booksContainer.children;
|
||
|
||
let visibleCount = 0;
|
||
for (let i = 0; i < bookCards.length; i++) {
|
||
const card = bookCards[i];
|
||
if (card.id === 'empty-state') continue;
|
||
|
||
const title = card.querySelector('.font-semibold')?.textContent.toLowerCase() || '';
|
||
const author = card.querySelector('.text-sm')?.textContent.toLowerCase() || '';
|
||
|
||
const matches = title.includes(searchTerm) || author.includes(searchTerm);
|
||
|
||
if (matches || searchTerm === '') {
|
||
card.style.display = '';
|
||
visibleCount++;
|
||
} else {
|
||
card.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
function showAddBooksModal() {
|
||
document.getElementById('add-books-modal').classList.remove('hidden');
|
||
selectedBooks.clear();
|
||
document.getElementById('book-results').innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Enter at least 2 characters to search.</p>';
|
||
}
|
||
|
||
function hideAddBooksModal() {
|
||
document.getElementById('add-books-modal').classList.add('hidden');
|
||
document.getElementById('book-search').value = '';
|
||
document.getElementById('book-results').innerHTML = '';
|
||
selectedBooks.clear();
|
||
}
|
||
|
||
function searchBooks() {
|
||
const searchTerm = document.getElementById('book-search').value;
|
||
const container = document.getElementById('book-results');
|
||
|
||
if (searchTerm.length < 2) {
|
||
container.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Enter at least 2 characters to search.</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Searching...</p>';
|
||
|
||
fetch(`/api/media-items/search?q=${encodeURIComponent(searchTerm)}`, {
|
||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.data && result.data.length > 0) {
|
||
let html = '<div class="space-y-2">';
|
||
result.data.slice(0, 50).forEach(book => {
|
||
const isSelected = selectedBooks.has(book.media_item_id);
|
||
const checkedAttr = isSelected ? 'checked' : '';
|
||
const authorHtml = book.author ? `<div class="text-xs" style="color: var(--text-secondary)">${book.author}</div>` : '';
|
||
html += `
|
||
<div class="flex items-center gap-3 p-2 rounded cursor-pointer hover:opacity-80"
|
||
style="background-color: var(--bg-primary);"
|
||
onclick="toggleBookSelection('${book.media_item_id}')">
|
||
<input type="checkbox" ${checkedAttr} class="w-4 h-4">
|
||
<img src="${book.cover_image_path || '/static/placeholder-book.svg'}"
|
||
alt="Cover" class="w-10 h-14 object-cover rounded">
|
||
<div class="flex-1">
|
||
<div class="text-sm font-medium" style="color: var(--text-primary)">${book.title}</div>
|
||
${authorHtml}
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
html += '</div>';
|
||
if (result.data.length > 50) {
|
||
html += '<p class="text-sm mt-2" style="color: var(--text-secondary)">Showing first 50 of ' + result.data.length + ' results</p>';
|
||
}
|
||
container.innerHTML = html;
|
||
} else {
|
||
container.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">No books found</p>';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
container.innerHTML = '<p class="text-sm" style="color: var(--error)">Failed to search books</p>';
|
||
});
|
||
}
|
||
|
||
function toggleBookSelection(bookId) {
|
||
if (selectedBooks.has(bookId)) {
|
||
selectedBooks.delete(bookId);
|
||
} else {
|
||
selectedBooks.add(bookId);
|
||
}
|
||
searchBooks();
|
||
}
|
||
|
||
function addSelectedBooks() {
|
||
if (selectedBooks.size === 0) {
|
||
showToast('Please select at least one book', 'error');
|
||
return;
|
||
}
|
||
|
||
const bookIds = Array.from(selectedBooks);
|
||
|
||
fetch(`/api/collections/${collectionId}/books`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||
},
|
||
body: JSON.stringify({ book_ids: bookIds })
|
||
})
|
||
.then(response => {
|
||
if (response.ok) {
|
||
showToast(`Added ${bookIds.length} book(s) to collection`, 'success');
|
||
hideAddBooksModal();
|
||
location.reload();
|
||
} else {
|
||
showToast('Failed to add books', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showToast('Failed to add books', 'error');
|
||
});
|
||
}
|
||
|
||
function removeBook(bookId) {
|
||
if (!confirm('Remove this book from the collection?')) return;
|
||
|
||
fetch(`/api/collections/${collectionId}/books/${bookId}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||
})
|
||
.then(response => {
|
||
if (response.ok) {
|
||
showToast('Book removed from collection', 'success');
|
||
location.reload();
|
||
} else {
|
||
showToast('Failed to remove book', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showToast('Failed to remove book', 'error');
|
||
});
|
||
}
|
||
|
||
function toggleBookForRemoval(bookId) {
|
||
if (booksToRemove.has(bookId)) {
|
||
booksToRemove.delete(bookId);
|
||
} else {
|
||
booksToRemove.add(bookId);
|
||
}
|
||
updateSelectedCount();
|
||
}
|
||
|
||
function updateSelectedCount() {
|
||
const count = booksToRemove.size;
|
||
const countSpan = document.getElementById('selected-count');
|
||
const removeBtn = document.getElementById('bulk-remove-btn');
|
||
|
||
if (count > 0) {
|
||
countSpan.textContent = count + ' selected';
|
||
countSpan.classList.remove('hidden');
|
||
removeBtn.disabled = false;
|
||
} else {
|
||
countSpan.classList.add('hidden');
|
||
removeBtn.disabled = true;
|
||
}
|
||
}
|
||
|
||
function removeSelectedBooks() {
|
||
if (booksToRemove.size === 0) {
|
||
showToast('No books selected', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`Remove ${booksToRemove.size} book(s) from the collection?`)) return;
|
||
|
||
const bookIds = Array.from(booksToRemove);
|
||
|
||
fetch(`/api/collections/${collectionId}/books/bulk-remove`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||
},
|
||
body: JSON.stringify({ book_ids: bookIds })
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.removed > 0) {
|
||
showToast(`Removed ${result.removed} book(s) from collection`, 'success');
|
||
location.reload();
|
||
} else {
|
||
showToast('Failed to remove books', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showToast('Failed to remove books', 'error');
|
||
});
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showToast('Failed to remove book', 'error');
|
||
});
|
||
}
|
||
|
||
function logout() {
|
||
localStorage.removeItem('token');
|
||
window.location.href = '/login';
|
||
}
|
||
|
||
renderBooks();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
}
|