406 lines
19 KiB
JavaScript
406 lines
19 KiB
JavaScript
let allLocations = []; // To store fetched locations globally - ADDED HERE
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
await fetchLocations(); // Fetch locations on page load
|
|
const bookListContainer = document.getElementById('book-list-container');
|
|
const modal = document.getElementById('edit-modal');
|
|
const closeModalButton = modal.querySelector('.close-button');
|
|
const modalBookTitle = document.getElementById('modal-book-title');
|
|
const modalBookIdInput = document.getElementById('modal-book-id');
|
|
|
|
const modalSearchQueryInput = document.getElementById('modal-search-query');
|
|
const modalSearchButton = document.getElementById('modal-search-button');
|
|
const modalSearchResultsContainer = document.getElementById('modal-search-results');
|
|
|
|
const modalEditLocationSelect = document.getElementById('modal-edit-location');
|
|
const modalEditFields = {
|
|
title: document.getElementById('modal-edit-title'),
|
|
authors: document.getElementById('modal-edit-authors'),
|
|
publishedDate: document.getElementById('modal-edit-publishedDate'),
|
|
isbn: document.getElementById('modal-edit-isbn'),
|
|
description: document.getElementById('modal-edit-description'),
|
|
pages: document.getElementById('modal-edit-pages'),
|
|
publishers: document.getElementById('modal-edit-publishers'),
|
|
subjects: document.getElementById('modal-edit-subjects'),
|
|
cover_small: document.getElementById('modal-edit-cover-small'),
|
|
cover_medium: document.getElementById('modal-edit-cover-medium')
|
|
};
|
|
const modalSaveButton = document.getElementById('modal-save-button');
|
|
|
|
let currentBooks = []; // To store the fetched books globally
|
|
// let allLocations = []; // To store fetched locations globally - REMOVED FROM HERE
|
|
let currentlyEditingBookId = null;
|
|
|
|
const addNewBookButton = document.getElementById('add-new-book-button');
|
|
|
|
addNewBookButton.addEventListener('click', () => {
|
|
currentlyEditingBookId = null; // Signal "add" mode
|
|
modalBookIdInput.value = ''; // Clear hidden book ID input
|
|
modalBookTitle.textContent = 'Add New Book'; // Update modal title for adding
|
|
|
|
// Clear all form fields
|
|
Object.values(modalEditFields).forEach(field => field.value = '');
|
|
modalEditLocationSelect.value = ''; // Reset location dropdown
|
|
modalSearchResultsContainer.innerHTML = ''; // Clear any previous search results
|
|
document.getElementById('modal-search-query').value = ''; // Clear search query too
|
|
|
|
openModal(); // Open the modal for new entry
|
|
});
|
|
|
|
// Authentication has been disabled for book updates
|
|
|
|
// --- Fetch and Display Books ---
|
|
async function fetchAndDisplayBooks() {
|
|
try {
|
|
const response = await fetch('/api/books');
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
currentBooks = await response.json();
|
|
renderBookList(currentBooks);
|
|
} catch (error) {
|
|
console.error('Error fetching books:', error);
|
|
bookListContainer.innerHTML = '<p>Error loading books. See console for details.</p>';
|
|
}
|
|
}
|
|
|
|
function renderBookList(books) {
|
|
bookListContainer.innerHTML = ''; // Clear existing list
|
|
if (books.length === 0) {
|
|
bookListContainer.innerHTML = '<p>No books found in the library.</p>';
|
|
return;
|
|
}
|
|
|
|
const ul = document.createElement('ul');
|
|
ul.className = 'book-list'; // Add a class for styling if needed
|
|
books.forEach(book => {
|
|
const li = document.createElement('li');
|
|
const locationName = book.Location ? `${book.Location.name}${book.Location.shelf ? ' (Shelf: ' + book.Location.shelf + ')' : ''}` : 'No Location'; // Reverted to book.Location (capital L)
|
|
li.innerHTML = `
|
|
<div class="book-entry">
|
|
<img src="${book.cover_small || 'placeholder.png'}" alt="Cover for ${book.title || 'N/A'}" style="width:50px; height:auto; margin-right:10px; flex-shrink: 0;">
|
|
<div style="flex-grow: 1;">
|
|
<p><strong>Title:</strong> ${book.title || 'N/A'}</p>
|
|
<p><strong>Author(s):</strong> ${book.authors || 'N/A'}</p>
|
|
<p><strong>Published:</strong> ${book.publishedDate || 'N/A'}</p>
|
|
<p><strong>ISBN:</strong> ${book.isbn || 'N/A'}</p>
|
|
<p><strong>Publisher(s):</strong> ${book.publishers || 'N/A'}</p>
|
|
<p><strong>Location:</strong> ${locationName}</p>
|
|
</div>
|
|
<button class="edit-metadata-button" data-book-id="${book.id}" style="margin-left: 10px; align-self: center;">Edit Metadata</button>
|
|
</div>
|
|
`;
|
|
ul.appendChild(li);
|
|
});
|
|
bookListContainer.appendChild(ul);
|
|
|
|
// Add event listeners to new edit buttons
|
|
document.querySelectorAll('.edit-metadata-button').forEach(button => {
|
|
button.addEventListener('click', handleEditMetadataClick);
|
|
});
|
|
}
|
|
|
|
// --- Modal Logic ---
|
|
function openModal() {
|
|
modal.style.display = 'block';
|
|
}
|
|
|
|
function closeModal() {
|
|
modal.style.display = 'none';
|
|
modalSearchResultsContainer.innerHTML = ''; // Clear search results
|
|
// Clear form fields
|
|
Object.values(modalEditFields).forEach(input => input.value = '');
|
|
modalSearchQueryInput.value = '';
|
|
}
|
|
|
|
closeModalButton.onclick = closeModal;
|
|
window.onclick = (event) => {
|
|
if (event.target == modal) {
|
|
closeModal();
|
|
}
|
|
};
|
|
|
|
async function handleEditMetadataClick(event) {
|
|
event.preventDefault(); // Prevent default action
|
|
const bookId = event.target.dataset.bookId;
|
|
const bookToEdit = currentBooks.find(b => b.id.toString() === bookId);
|
|
|
|
if (!bookToEdit) {
|
|
alert('Book not found!');
|
|
return;
|
|
}
|
|
|
|
modalBookTitle.textContent = bookToEdit.title;
|
|
modalBookIdInput.value = bookToEdit.id;
|
|
currentlyEditingBookId = bookToEdit.id; // Ensure this is set for edit mode
|
|
|
|
// Pre-fill search query and existing data
|
|
modalSearchQueryInput.value = bookToEdit.title || '';
|
|
if (bookToEdit.isbn) {
|
|
modalSearchQueryInput.value += ` ISBN: ${bookToEdit.isbn}`;
|
|
}
|
|
|
|
modalEditFields.title.value = bookToEdit.title || '';
|
|
modalEditFields.authors.value = bookToEdit.authors || '';
|
|
modalEditFields.publishedDate.value = bookToEdit.publishedDate || '';
|
|
modalEditFields.isbn.value = bookToEdit.isbn || '';
|
|
modalEditFields.description.value = bookToEdit.description || '';
|
|
modalEditFields.pages.value = bookToEdit.number_of_pages || '';
|
|
modalEditFields.publishers.value = bookToEdit.publishers || '';
|
|
modalEditFields.subjects.value = bookToEdit.subjects || '';
|
|
modalEditFields.cover_small.value = bookToEdit.cover_small || '';
|
|
modalEditFields.cover_medium.value = bookToEdit.cover_medium || '';
|
|
|
|
// Populate and set location dropdown
|
|
modalEditLocationSelect.innerHTML = '<option value="">-- No Location --</option>'; // Clear existing options but keep default
|
|
allLocations.forEach(loc => {
|
|
const option = document.createElement('option');
|
|
option.value = loc.id;
|
|
option.textContent = `${loc.name}` + (loc.shelf ? ` (Shelf: ${loc.shelf})` : '');
|
|
modalEditLocationSelect.appendChild(option);
|
|
});
|
|
modalEditLocationSelect.value = bookToEdit.location_id || (bookToEdit.Location ? bookToEdit.Location.id : '');
|
|
|
|
openModal();
|
|
}
|
|
|
|
// --- Google Books API Search in Modal ---
|
|
modalSearchButton.addEventListener('click', handleModalSearch);
|
|
|
|
async function handleModalSearch() {
|
|
let originalQuery = modalSearchQueryInput.value.trim();
|
|
if (!originalQuery) {
|
|
alert('Please enter a search query.');
|
|
return;
|
|
}
|
|
|
|
let searchQuery = originalQuery;
|
|
const potentialIsbn = originalQuery.replace(/-/g, '');
|
|
let isIsbnQuery = false;
|
|
|
|
if (/^(\d{9}[\dX]|\d{13})$/.test(potentialIsbn)) {
|
|
searchQuery = potentialIsbn; // Use the cleaned ISBN for OpenLibrary
|
|
isIsbnQuery = true;
|
|
console.log(`Detected ISBN-like string "${originalQuery}", searching as ISBN "${searchQuery}"`);
|
|
} else if (originalQuery.toLowerCase().startsWith('isbn:')) {
|
|
searchQuery = originalQuery.substring(5).replace(/-/g, ''); // Remove 'isbn:' and hyphens
|
|
isIsbnQuery = true;
|
|
console.log(`Query "${originalQuery}" formatted for ISBN search, using "${searchQuery}"`);
|
|
} else {
|
|
console.log(`Performing general search for "${originalQuery}"`);
|
|
}
|
|
|
|
modalSearchResultsContainer.innerHTML = '<p>Searching...</p>';
|
|
const searchButton = document.getElementById('modal-search-button');
|
|
if (searchButton) searchButton.disabled = true;
|
|
|
|
let combinedResults = [];
|
|
|
|
try {
|
|
const googleQuery = isIsbnQuery ? `isbn:${searchQuery}` : searchQuery;
|
|
const googleUrl = `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(googleQuery)}&maxResults=5`;
|
|
const openLibFields = 'key,title,author_name,first_publish_year,isbn,cover_i,publisher,subject,number_of_pages_median,subtitle,first_sentence_value';
|
|
const openLibUrl = `https://openlibrary.org/search.json?q=${encodeURIComponent(searchQuery)}&limit=5&fields=${openLibFields}`;
|
|
|
|
console.log("Searching Google Books API with URL:", googleUrl);
|
|
console.log("Searching OpenLibrary API with URL:", openLibUrl);
|
|
|
|
const [googleResult, openLibResult] = await Promise.allSettled([
|
|
fetch(googleUrl).then(res => res.ok ? res.json() : Promise.reject(new Error(`Google Books API request failed: ${res.status} ${res.statusText}`))),
|
|
fetch(openLibUrl).then(res => res.ok ? res.json() : Promise.reject(new Error(`OpenLibrary API request failed: ${res.status} ${res.statusText}`)))
|
|
]);
|
|
|
|
if (googleResult.status === 'fulfilled' && googleResult.value.items) {
|
|
const googleBooks = googleResult.value.items.map(item => {
|
|
const volumeInfo = item.volumeInfo;
|
|
let isbn13 = '', isbn10 = '';
|
|
if (volumeInfo.industryIdentifiers) {
|
|
volumeInfo.industryIdentifiers.forEach(id => {
|
|
if (id.type === 'ISBN_13') isbn13 = id.identifier;
|
|
if (id.type === 'ISBN_10') isbn10 = id.identifier;
|
|
});
|
|
}
|
|
return {
|
|
title: volumeInfo.title,
|
|
authors: volumeInfo.authors ? volumeInfo.authors.join(', ') : '',
|
|
publishedDate: volumeInfo.publishedDate,
|
|
isbn: isbn13 || isbn10,
|
|
description: volumeInfo.description,
|
|
pageCount: volumeInfo.pageCount,
|
|
publishers: volumeInfo.publisher ? (Array.isArray(volumeInfo.publisher) ? volumeInfo.publisher.join(', ') : volumeInfo.publisher) : '',
|
|
subjects: volumeInfo.categories ? volumeInfo.categories.join(', ') : '',
|
|
cover_small: volumeInfo.imageLinks?.smallThumbnail,
|
|
cover_medium: volumeInfo.imageLinks?.thumbnail,
|
|
source: 'Google Books',
|
|
rawData: volumeInfo
|
|
};
|
|
});
|
|
combinedResults.push(...googleBooks);
|
|
} else if (googleResult.status === 'rejected') {
|
|
console.error('Error fetching from Google Books:', googleResult.reason);
|
|
}
|
|
|
|
if (openLibResult.status === 'fulfilled' && openLibResult.value.docs) {
|
|
const openLibBooks = openLibResult.value.docs.map(doc => {
|
|
return {
|
|
title: doc.title,
|
|
authors: doc.author_name ? doc.author_name.join(', ') : '',
|
|
publishedDate: doc.first_publish_year ? String(doc.first_publish_year) : '',
|
|
isbn: doc.isbn ? doc.isbn[0] : '', // Take first ISBN
|
|
description: doc.first_sentence_value || doc.subtitle || '',
|
|
pageCount: doc.number_of_pages_median,
|
|
publishers: doc.publisher ? doc.publisher.join(', ') : '',
|
|
subjects: doc.subject ? doc.subject.join(', ') : '',
|
|
cover_small: doc.cover_i ? `https://covers.openlibrary.org/b/id/${doc.cover_i}-S.jpg` : '',
|
|
cover_medium: doc.cover_i ? `https://covers.openlibrary.org/b/id/${doc.cover_i}-M.jpg` : '',
|
|
source: 'OpenLibrary',
|
|
rawData: doc
|
|
};
|
|
});
|
|
combinedResults.push(...openLibBooks);
|
|
}
|
|
else if (openLibResult.status === 'rejected') {
|
|
console.error('Error fetching from OpenLibrary:', openLibResult.reason);
|
|
}
|
|
|
|
displayModalSearchResults(combinedResults);
|
|
|
|
} catch (error) { // Catch any unexpected errors during the setup or Promise.allSettled itself
|
|
console.error('Error in handleModalSearch:', error);
|
|
modalSearchResultsContainer.innerHTML = `<p>An unexpected error occurred during search: ${error.message}. Please check console.</p>`;
|
|
} finally {
|
|
if (searchButton) searchButton.disabled = false;
|
|
}
|
|
}
|
|
|
|
function displayModalSearchResults(items) {
|
|
modalSearchResultsContainer.innerHTML = '';
|
|
if (!items || items.length === 0) {
|
|
modalSearchResultsContainer.innerHTML = '<p>No results found from any source.</p>';
|
|
return;
|
|
}
|
|
|
|
const ul = document.createElement('ul');
|
|
items.forEach(bookItem => {
|
|
const li = document.createElement('li');
|
|
li.innerHTML = `
|
|
<strong>${bookItem.title || 'N/A'}</strong> by ${bookItem.authors || 'N/A'} (<em>${bookItem.source}</em>)
|
|
<br>
|
|
Published: ${bookItem.publishedDate || 'N/A'}, ISBN: ${bookItem.isbn || 'N/A'}
|
|
<button class="select-book-result">Use this</button>
|
|
`;
|
|
li.querySelector('.select-book-result').addEventListener('click', () => {
|
|
populateFormWithSelectedBook(bookItem);
|
|
});
|
|
ul.appendChild(li);
|
|
});
|
|
modalSearchResultsContainer.appendChild(ul);
|
|
}
|
|
|
|
function populateFormWithSelectedBook(bookItem) {
|
|
modalEditFields.title.value = bookItem.title || '';
|
|
modalEditFields.authors.value = bookItem.authors || '';
|
|
modalEditFields.publishedDate.value = bookItem.publishedDate || '';
|
|
modalEditFields.isbn.value = bookItem.isbn || '';
|
|
modalEditFields.description.value = bookItem.description || '';
|
|
modalEditFields.pages.value = bookItem.pageCount || '';
|
|
modalEditFields.publishers.value = bookItem.publishers || '';
|
|
modalEditFields.subjects.value = bookItem.subjects || '';
|
|
modalEditFields.cover_small.value = bookItem.cover_small || '';
|
|
modalEditFields.cover_medium.value = bookItem.cover_medium || '';
|
|
}
|
|
|
|
// --- Save Changes ---
|
|
modalSaveButton.addEventListener('click', async () => {
|
|
const bookData = {
|
|
title: modalEditFields.title.value.trim(),
|
|
authors: modalEditFields.authors.value.trim(),
|
|
publishedDate: modalEditFields.publishedDate.value.trim(),
|
|
isbn: modalEditFields.isbn.value.trim(),
|
|
description: modalEditFields.description.value.trim(),
|
|
number_of_pages: modalEditFields.pages.value ? parseInt(modalEditFields.pages.value, 10) : null,
|
|
publishers: modalEditFields.publishers.value.trim(),
|
|
subjects: modalEditFields.subjects.value.trim(),
|
|
cover_small: modalEditFields.cover_small.value.trim(),
|
|
cover_medium: modalEditFields.cover_medium.value.trim(),
|
|
location_id: modalEditLocationSelect.value || null,
|
|
};
|
|
|
|
if (!bookData.title) {
|
|
alert('Title is required.');
|
|
return;
|
|
}
|
|
|
|
let url;
|
|
let method;
|
|
const headers = {
|
|
'Content-Type': 'application/json'
|
|
};
|
|
|
|
if (currentlyEditingBookId) { // Edit mode
|
|
url = `/api/books/${currentlyEditingBookId}`;
|
|
method = 'PUT';
|
|
// Basic Auth not currently implemented for PUT as per prior setup
|
|
} else { // Add mode
|
|
url = '/api/books';
|
|
method = 'POST';
|
|
// POST endpoint requires Basic Auth
|
|
const adminPassword = prompt("Enter admin password to add a new book:");
|
|
if (adminPassword === null) { // User cancelled prompt
|
|
return;
|
|
}
|
|
headers['Authorization'] = 'Basic ' + btoa('admin:' + adminPassword);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: method,
|
|
headers: headers,
|
|
body: JSON.stringify(bookData)
|
|
});
|
|
|
|
const responseBody = await response.text(); // Read body as text first to handle potential non-JSON errors
|
|
let result;
|
|
try {
|
|
result = JSON.parse(responseBody);
|
|
} catch (e) {
|
|
// If parsing fails, the response was not valid JSON
|
|
console.error('Failed to parse server response as JSON:', responseBody);
|
|
throw new Error(`Server returned non-JSON response (status: ${response.status}). Check console for details.`);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorMessage = result && result.error ? result.error : `HTTP error! Status: ${response.status}`;
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
if (result.success) {
|
|
alert(`Book ${method === 'POST' ? 'added' : 'updated'} successfully!`);
|
|
closeModal();
|
|
fetchAndDisplayBooks(); // Refresh the list
|
|
} else {
|
|
alert(`Failed to ${method === 'POST' ? 'add' : 'update'} book: ${result.error || 'Unknown server error'}`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error ${method === 'POST' ? 'adding' : 'saving'} book data:`, error);
|
|
alert(`Error ${method === 'POST' ? 'adding' : 'saving'}: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
// Initial load
|
|
fetchAndDisplayBooks();
|
|
});
|
|
|
|
async function fetchLocations() {
|
|
try {
|
|
const response = await fetch('/api/locations');
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch locations: ${response.status}`);
|
|
}
|
|
allLocations = await response.json();
|
|
console.log('Locations fetched:', allLocations);
|
|
} catch (error) {
|
|
console.error('Error fetching locations:', error);
|
|
// Optionally, inform the user that locations could not be loaded
|
|
}
|
|
}
|