diff --git a/bookHelpers.js b/bookHelpers.js
index 3c0304f..c6701f8 100644
--- a/bookHelpers.js
+++ b/bookHelpers.js
@@ -3,7 +3,13 @@ const axios = require('axios');
// Fetch book from the local database by ISBN
const fetchBookFromLocalDatabase = async (isbn) => {
- return await Book.findOne({ where: { isbn } });
+ return await Book.findOne({
+ where: { isbn },
+ include: [{
+ model: Location,
+ as: 'Location'
+ }]
+ });
};
// Fetch book from Google Books API
diff --git a/books.db b/books.db
index 2ce00de..907d07c 100644
Binary files a/books.db and b/books.db differ
diff --git a/index.js b/index.js
index 668e6a2..d4a7dcb 100644
--- a/index.js
+++ b/index.js
@@ -130,6 +130,16 @@ app.get('/api/books-with-images', async (req, res) => {
},
attributes: ['isbn', 'title', 'cover_small', 'cover_medium', 'cover_large', 'authors'],
});
+ // CORRECTED DEBUG LOGGING ON SERVER
+ if (books && books.length > 0) {
+ // Log a specific book if its ID is known, e.g., ID 207 for Artemis Fowl
+ const specificBookForDebug = books.find(b => b.id === 207);
+ if (specificBookForDebug) {
+ console.log("Server-side specific book from /api/books (ID 207):", JSON.stringify(specificBookForDebug, null, 2));
+ } else {
+ console.log("Server-side book ID 207 not found in /api/books results, logging first book instead:", JSON.stringify(books[0], null, 2));
+ }
+ }
res.json(books);
} catch (error) {
console.error('Failed to fetch books with images:', error);
@@ -301,23 +311,25 @@ function delay(ms) {
// Confirm book is in the library by ISBN
app.get('/book/confirm/:isbn', async (req, res) => {
const { isbn } = req.params;
- console.log(`Fetching book data for ISBN: ${isbn}`);
+ console.log(`Cascade: /book/confirm/:isbn - Confirming ISBN: ${isbn}`);
try {
// Check if the book is in the local database
const localBook = await fetchBookFromLocalDatabase(isbn);
if (localBook) {
- console.log('Book found in the local database');
+ console.log('Cascade: /book/confirm/:isbn - Book found in local database.');
return res.json({ source: 'local', data: localBook });
+ } else {
+ console.log('Cascade: /book/confirm/:isbn - Book NOT found in local database.');
+ return res.status(404).json({ error: 'Book not found in local library' });
}
} catch (error) {
- console.error(error);
- return res.status(500).json({ error: 'Failed to fetch book data' });
+ console.error('Cascade: /book/confirm/:isbn - Error during lookup:', error);
+ return res.status(500).json({ error: 'Failed to confirm book in local library' });
}
});
-
app.get('/search-title', async (req, res) => {
const { title, internalOnly = false } = req.query;
console.log(`Searching for books by title or related fields: ${title}`);
@@ -393,9 +405,14 @@ app.delete('/book/:id', async (req, res) => {
}
});
-app.get('/locations', async (req, res) => {
+app.get('/api/locations', async (req, res) => {
try {
- const locations = await Location.findAll();
+ const locations = await Location.findAll({
+ order: [
+ ['name', 'ASC'],
+ ['shelf', 'ASC']
+ ]
+ });
res.json(locations);
} catch (error) {
console.error('Failed to fetch locations:', error);
@@ -657,6 +674,156 @@ function requestCheckout(isbn) {
});
}
+// API endpoint to get all books
+app.get('/api/books', async (req, res) => {
+ try {
+ const books = await Book.findAll({
+ include: [{
+ model: Location,
+ as: 'Location' // Corrected alias based on model definition
+ }],
+ order: [['title', 'ASC']]
+ });
+ res.json(books.map(book => ({
+ id: book.id,
+ title: book.title,
+ authors: book.authors,
+ isbn: book.isbn,
+ publishedDate: book.publishedDate,
+ description: book.description,
+ cover_small: book.cover_small,
+ number_of_pages: book.number_of_pages,
+ publishers: book.publishers,
+ subjects: book.subjects,
+ location_id: book.location_id,
+ Location: book.Location ? { id: book.Location.id, name: book.Location.name, room: book.Location.room, shelf: book.Location.shelf } : null
+ })));
+ } catch (error) {
+ console.error('Failed to fetch books:', error);
+ res.status(500).json({ error: 'Failed to fetch books' });
+ }
+});
+
+// API endpoint to add a new book
+app.post('/api/books', async (req, res) => {
+ try {
+ const {
+ title,
+ authors,
+ publishedDate,
+ description,
+ isbn,
+ number_of_pages,
+ publishers,
+ subjects,
+ cover_small,
+ cover_medium,
+ cover_large,
+ location_id
+ } = req.body;
+
+ // Basic validation (e.g., title is required)
+ if (!title) {
+ return res.status(400).json({ error: 'Title is required' });
+ }
+
+ const newBookData = {
+ title,
+ authors: Array.isArray(authors) ? authors.join(', ') : authors,
+ publishedDate,
+ description,
+ isbn,
+ number_of_pages,
+ publishers: Array.isArray(publishers) ? publishers.join(', ') : publishers,
+ subjects: Array.isArray(subjects) ? subjects.join(', ') : subjects,
+ cover_small,
+ cover_medium,
+ cover_large,
+ location_id: location_id === '' ? null : location_id // Handle empty string as null
+ };
+
+ const book = await Book.create(newBookData);
+
+ // Fetch the newly created book with its location to send back in the response
+ const createdBookWithLocation = await Book.findByPk(book.id, {
+ include: [{
+ model: Location,
+ as: 'Location'
+ }]
+ });
+
+ res.status(201).json({ success: true, book: createdBookWithLocation });
+ } catch (error) {
+ console.error('Failed to add new book:', error);
+ if (error.name === 'SequelizeUniqueConstraintError') {
+ let errorMessage = 'Failed to add new book due to a conflict.';
+ // Assuming 'isbn' is a unique field in your Book model
+ if (error.fields && typeof error.fields === 'object' && 'isbn' in error.fields) {
+ errorMessage = 'A book with this ISBN already exists. Please use the edit feature if you want to update it.';
+ }
+ return res.status(409).json({ error: errorMessage });
+ }
+ res.status(500).json({ error: 'Failed to add new book' });
+ }
+});
+
+// API endpoint to update a book's metadata by its primary key ID
+app.put('/api/books/:id', async (req, res) => {
+ try {
+ const bookId = req.params.id;
+ const book = await Book.findByPk(bookId);
+
+ if (!book) {
+ return res.status(404).json({ error: 'Book not found' });
+ }
+
+ const {
+ title,
+ authors,
+ publishedDate,
+ description,
+ isbn,
+ number_of_pages,
+ publishers,
+ subjects,
+ cover_small,
+ cover_medium,
+ cover_large,
+ location_id // Added to handle location updates
+ } = req.body;
+
+ const updateData = {};
+ if (title !== undefined) updateData.title = title;
+ if (authors !== undefined) updateData.authors = Array.isArray(authors) ? authors.join(', ') : authors;
+ if (publishedDate !== undefined) updateData.publishedDate = publishedDate;
+ if (description !== undefined) updateData.description = description;
+ if (isbn !== undefined) updateData.isbn = isbn === '' ? null : isbn;
+ if (number_of_pages !== undefined) updateData.number_of_pages = number_of_pages;
+ if (publishers !== undefined) updateData.publishers = Array.isArray(publishers) ? publishers.join(', ') : publishers;
+ if (subjects !== undefined) updateData.subjects = Array.isArray(subjects) ? subjects.join(', ') : subjects;
+ if (cover_small !== undefined) updateData.cover_small = cover_small;
+ if (cover_medium !== undefined) updateData.cover_medium = cover_medium;
+ if (cover_large !== undefined) updateData.cover_large = cover_large;
+ if (location_id !== undefined) updateData.location_id = location_id === '' ? null : location_id; // Handle empty string as null
+
+ await book.update(updateData);
+ console.log(`Book data for ID ${bookId} updated successfully.`);
+
+ // Fetch the updated book with its location to send back in the response
+ const updatedBookWithLocation = await Book.findByPk(bookId, {
+ include: [{
+ model: Location,
+ as: 'Location'
+ }]
+ });
+ res.json({ success: true, book: updatedBookWithLocation });
+
+ } catch (error) {
+ console.error(`Failed to update book with ID ${req.params.id}:`, error);
+ res.status(500).json({ error: 'Failed to update book' });
+ }
+});
+
const httpsServer = https.createServer(credentials, app);
@@ -698,7 +865,7 @@ app.get('/api/books-on-loan', async (req, res) => {
}
});
-app.post('/book/update/:isbn', async (req, res) => {
+app.post('/book/update/:isbn', authMiddleware, async (req, res) => { // Added authMiddleware for consistency
const { isbn } = req.params;
const googleBooksApiKey = process.env.GOOGLE_BOOKS_API_KEY;
diff --git a/populate_locations.js b/populate_locations.js
new file mode 100644
index 0000000..eae6099
--- /dev/null
+++ b/populate_locations.js
@@ -0,0 +1,53 @@
+
+import { sequelize, Location } from './models.js';
+
+const locationsToCreate = [];
+
+// Cubes 1-10
+for (let i = 1; i <= 10; i++) {
+ locationsToCreate.push({ name: 'Cube', shelf: i.toString() });
+}
+
+// Office Shelves 1-6
+for (let i = 1; i <= 6; i++) {
+ locationsToCreate.push({ name: 'Office Shelf', shelf: i.toString() });
+}
+
+// Miscellaneous placements
+locationsToCreate.push({ name: 'Upstairs', shelf: 'Misc' });
+locationsToCreate.push({ name: 'Downstairs', shelf: 'Misc' });
+
+async function populateLocations() {
+ try {
+ // Optional: Sync database to ensure tables are created
+ await sequelize.sync(); // Often done in your main app setup
+
+ // Check if locations already exist to avoid duplicates if script is run multiple times
+ // This is a simple check; more robust checks might be needed for production
+ const existingLocations = await Location.findAll();
+ const newLocations = locationsToCreate.filter(ltc =>
+ !existingLocations.some(el => el.name === ltc.name && el.shelf === ltc.shelf)
+ );
+
+ if (newLocations.length > 0) {
+ await Location.bulkCreate(newLocations);
+ console.log(`${newLocations.length} locations have been successfully added.`);
+ } else {
+ console.log('All specified locations already exist in the database. No new locations added.');
+ }
+
+ // Select all locations from the database
+ const [locationsFromQuery] = await sequelize.query('SELECT * FROM locations'); // sequelize.query returns [results, metadata]
+ console.log('All locations:', locationsFromQuery);
+ } catch (error) {
+ console.error('Error populating locations:', error);
+ } finally {
+ // Close the database connection if the script is standalone and not part of a larger app run
+ if (sequelize && typeof sequelize.close === 'function') {
+ await sequelize.close();
+ console.log('Database connection closed.');
+ }
+ }
+}
+
+populateLocations();
diff --git a/public/edit_books.html b/public/edit_books.html
new file mode 100644
index 0000000..dd2a86c
--- /dev/null
+++ b/public/edit_books.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+ Edit Book Metadata
+
+
+ Edit Book Metadata
+
+
+
+
+
+
+
+
+
+
×
+
Edit Metadata for
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Selected Book Details (Editable)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/edit_books.js b/public/edit_books.js
new file mode 100644
index 0000000..8847a5b
--- /dev/null
+++ b/public/edit_books.js
@@ -0,0 +1,405 @@
+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 = 'Error loading books. See console for details.
';
+ }
+ }
+
+ function renderBookList(books) {
+ bookListContainer.innerHTML = ''; // Clear existing list
+ if (books.length === 0) {
+ bookListContainer.innerHTML = 'No books found in the library.
';
+ 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 = `
+
+

+
+
Title: ${book.title || 'N/A'}
+
Author(s): ${book.authors || 'N/A'}
+
Published: ${book.publishedDate || 'N/A'}
+
ISBN: ${book.isbn || 'N/A'}
+
Publisher(s): ${book.publishers || 'N/A'}
+
Location: ${locationName}
+
+
+
+ `;
+ 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 = ''; // 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 = 'Searching...
';
+ 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 = `An unexpected error occurred during search: ${error.message}. Please check console.
`;
+ } finally {
+ if (searchButton) searchButton.disabled = false;
+ }
+ }
+
+ function displayModalSearchResults(items) {
+ modalSearchResultsContainer.innerHTML = '';
+ if (!items || items.length === 0) {
+ modalSearchResultsContainer.innerHTML = 'No results found from any source.
';
+ return;
+ }
+
+ const ul = document.createElement('ul');
+ items.forEach(bookItem => {
+ const li = document.createElement('li');
+ li.innerHTML = `
+ ${bookItem.title || 'N/A'} by ${bookItem.authors || 'N/A'} (${bookItem.source})
+
+ Published: ${bookItem.publishedDate || 'N/A'}, ISBN: ${bookItem.isbn || 'N/A'}
+
+ `;
+ 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
+ }
+}
diff --git a/public/google_api_tester.html b/public/google_api_tester.html
new file mode 100644
index 0000000..9a48932
--- /dev/null
+++ b/public/google_api_tester.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+ Google Books API Tester
+
+
+ Google Books API Tester
+
+
+
+
+
+
+
+ API Response:
+
+
+
+
+
diff --git a/public/google_api_tester.js b/public/google_api_tester.js
new file mode 100644
index 0000000..289d6fb
--- /dev/null
+++ b/public/google_api_tester.js
@@ -0,0 +1,30 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const searchButton = document.getElementById('search-button');
+ const searchQueryInput = document.getElementById('search-query');
+ const apiResponseOutput = document.getElementById('api-response');
+
+ searchButton.addEventListener('click', async () => {
+ const query = searchQueryInput.value.trim();
+ if (!query) {
+ apiResponseOutput.textContent = 'Please enter a search query.';
+ return;
+ }
+
+ apiResponseOutput.textContent = 'Searching...';
+
+ try {
+ // Note: For extensive use, you should use an API key.
+ // Add &key=YOUR_API_KEY to the URL if you have one.
+ const response = await fetch(`https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(query)}`);
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
+ }
+ const data = await response.json();
+ apiResponseOutput.textContent = JSON.stringify(data, null, 2);
+ } catch (error) {
+ console.error('Error fetching from Google Books API:', error);
+ apiResponseOutput.textContent = `Error: ${error.message}`;
+ }
+ });
+});
diff --git a/public/scanner.html b/public/scanner.html
index 42397bb..6e45f34 100644
--- a/public/scanner.html
+++ b/public/scanner.html
@@ -13,7 +13,9 @@
- s
+
+
+
diff --git a/public/script.js b/public/script.js
index 62b302d..a8beb17 100644
--- a/public/script.js
+++ b/public/script.js
@@ -50,9 +50,8 @@ function startScanner() {
return;
}
- if (quaggaInitialized) {
- Quagga.start();
- } else {
+ const initLogic = () => {
+ console.log("startScanner/initLogic: Proceeding with Quagga.init().");
Quagga.init({
inputStream: {
name: "Live",
@@ -61,34 +60,93 @@ function startScanner() {
constraints: {
deviceId: selectedDeviceId,
facingMode: "environment",
- // Remove advanced constraints if not needed
+ advanced: [{torch: document.getElementById('flash-toggle') ? document.getElementById('flash-toggle').checked : false}]
},
},
decoder: {
readers: ["ean_reader", "ean_8_reader"]
}
- }, function (err) {
+ }, function(err) {
if (err) {
- console.log(err);
+ console.error("Quagga.init() callback: Failed with error:", err);
+ quaggaInitialized = false;
+ alert('Error initializing scanner: ' + err.name + ' - ' + err.message);
return;
}
- console.log("Initialization finished. Ready to start");
+ console.log("Quagga.init() callback: Successful. Starting stream...");
Quagga.start();
+ console.log("Quagga.init() callback: Quagga.start() called.");
quaggaInitialized = true;
-
- // Set up the onDetected handler
+ Quagga.offDetected(processBarcode); // Remove before adding to prevent duplicates
Quagga.onDetected(processBarcode);
+ console.log("Cascade: startScanner/initLogic - Quagga.onDetected(processBarcode) RE-ATTACHED.");
+ console.log("Quagga.init() callback: Stream started. onDetected handler active.");
+
+ // Explicitly re-apply flash if toggle is on, after stream has started
+ const flashToggleEl = document.getElementById('flash-toggle');
+ if (flashToggleEl && flashToggleEl.checked) {
+ console.log("Cascade: startScanner/initLogic - Flash toggle is checked. Attempting to re-apply torch constraint post-start.");
+ if (Quagga.CameraAccess && Quagga.CameraAccess.getActiveTrack()) {
+ const track = Quagga.CameraAccess.getActiveTrack();
+ track.applyConstraints({ advanced: [{ torch: true }] })
+ .then(() => {
+ console.log("Cascade: startScanner/initLogic - Torch constraint explicitly re-applied successfully post-start.");
+ })
+ .catch(constraintErr => {
+ console.error("Cascade: startScanner/initLogic - Error explicitly re-applying torch constraint post-start:", constraintErr);
+ });
+ } else {
+ console.warn("Cascade: startScanner/initLogic - Flash toggle checked, but cannot get active track to re-apply torch constraint post-start.");
+ }
+ }
});
+ };
+
+ if (quaggaInitialized) {
+ console.log("startScanner: Restart requested. Stopping current Quagga instance.");
+ try {
+ Quagga.stop();
+ console.log("startScanner: Quagga.stop() called successfully during restart.");
+ } catch (e) {
+ console.warn("startScanner: Error during Quagga.stop(), proceeding with re-init attempt:", e);
+ }
+ quaggaInitialized = false; // Force re-init
+
+ console.log("startScanner: Delaying for 100ms before re-initializing Quagga for restart.");
+ setTimeout(() => {
+ console.log("startScanner: Delay finished. Calling initLogic for restart.");
+ initLogic();
+ }, 100); // 100ms delay
+ } else {
+ console.log("startScanner: Initial Quagga setup or forced re-init without prior true state.");
+ initLogic();
}
}
async function processBarcode(data) {
- if (isScanning) return; // Prevent further scans while a scan is being processed
+ console.log("Cascade: processBarcode - Entered. Current isScanning state:", isScanning);
+ if (isScanning) {
+ console.log("Cascade: processBarcode - Exiting early because isScanning is true.");
+ return; // Prevent further scans while a scan is being processed
+ }
isScanning = true; // Set the scanning flag
+ console.log("Cascade: processBarcode - Set isScanning to true.");
const isbn = data.codeResult.code;
- console.log("Detected ISBN:", isbn);
- Quagga.stop(); // Stop the scanner once an ISBN is detected
+ console.log("Detected code:", isbn);
+
+ // Validate ISBN length (10 or 13 digits)
+ if (!isbn || (isbn.length !== 10 && isbn.length !== 13)) {
+ console.log(`Cascade: processBarcode - Invalid code length: ${isbn.length}. Code: ${isbn}. Resuming scan.`);
+ // isScanning = false; // Reset isScanning to allow immediate re-scan by Quagga's internal loop if needed
+ // No need to explicitly set isScanning to false here if we don't stop Quagga.
+ // The initial check `if (isScanning)` at the top of processBarcode should handle concurrent calls.
+ // We want Quagga to continue, so we don't call Quagga.stop() or startScanner() here.
+ return; // Continue scanning
+ }
+
+ console.log("Validated ISBN:", isbn);
+ Quagga.stop(); // Stop the scanner once a VALID ISBN is detected
if (document.getElementById('confirm-mode').checked) {
// Confirm book in library
@@ -110,20 +168,25 @@ async function fetchBookInfo(isbn) {
bookData.isbn2 = isbn; // Add the ISBN to the book data
promptUserWithBook(bookData);
} else {
- console.log("No book data found. Restarting scanner...");
+ console.log("Cascade: fetchBookInfo - No book data. Resetting isScanning.");
+ isScanning = false; // Reset flag BEFORE restarting scanner
+ console.log("Cascade: fetchBookInfo - isScanning is now false. Calling startScanner...");
startScanner(); // Restart the scanner if no book information is found
}
} catch (error) {
console.error('Error fetching book data:', error);
+ console.log("Cascade: fetchBookInfo - Error fetching. Resetting isScanning.");
+ isScanning = false; // Reset flag BEFORE restarting scanner
+ console.log("Cascade: fetchBookInfo - isScanning is now false (error path). Calling startScanner...");
startScanner(); // Restart the scanner on error as well
}
}
function promptUserWithBook(bookData) {
// Prepare the information to display in the confirm dialog
- const title = bookData.title;
- const authors = bookData.authors ? bookData.authors.join(', ') : 'Unknown Author';
- const description = bookData.description || 'No description available';
+ const title = bookData.data.title;
+ const authors = bookData.data.authors ? (Array.isArray(bookData.data.authors) ? bookData.data.authors.join(', ') : bookData.data.authors) : 'Unknown Author';
+ const description = bookData.data.description || 'No description available';
// Combine the information into a single message
const message = `Title: ${title}\nAuthor(s): ${authors}\nDescription: ${description}\n\nDo you want to add this book to the database?`;
@@ -184,12 +247,13 @@ async function confirmBookInLibrary(isbn) {
try {
const response = await fetch(`/book/confirm/${isbn}`);
const bookData = await response.json();
+ console.log("Cascade: confirmBookInLibrary - Response from /book/confirm:", bookData);
- if (bookData.title) {
+ if (bookData.data && bookData.data.title) {
// Display the book information and a success message
- const title = bookData.title;
- const authors = bookData.authors ? bookData.authors.join(', ') : 'Unknown Author';
- const description = bookData.description || 'No description available';
+ const title = bookData.data.title;
+ const authors = bookData.data.authors ? (Array.isArray(bookData.data.authors) ? bookData.data.authors.join(', ') : bookData.data.authors) : 'Unknown Author';
+ const description = bookData.data.description || 'No description available';
const message = `Title: ${title}\nAuthor(s): ${authors}\nDescription: ${description}\n\nBook found in the library!`;
@@ -201,6 +265,9 @@ async function confirmBookInLibrary(isbn) {
console.error('Error confirming book in library:', error);
alert('An error occurred while confirming the book in the library.');
} finally {
+ console.log("Cascade: confirmBookInLibrary - finally block. Resetting isScanning.");
+ isScanning = false; // Reset flag BEFORE restarting scanner
+ console.log("Cascade: confirmBookInLibrary - isScanning is now false. Calling startScanner...");
startScanner(); // Restart the scanner after processing
}
}
@@ -295,4 +362,34 @@ function selectBook(index) {
window.onload = function() {
getCameras();
searchByTitle('');
+
+ const flashToggle = document.getElementById('flash-toggle');
+ if (flashToggle) {
+ flashToggle.addEventListener('change', function() {
+ if (quaggaInitialized && Quagga.CameraAccess && Quagga.CameraAccess.getActiveTrack()) {
+ const track = Quagga.CameraAccess.getActiveTrack();
+ const constraints = { advanced: [{ torch: this.checked }] };
+ track.applyConstraints(constraints)
+ .then(() => {
+ console.log("Flash toggled via applyConstraints:", this.checked);
+ })
+ .catch(err => {
+ console.error("Error applying flash constraint dynamically:", err, ". Falling back to scanner restart.");
+ // Fallback: restart scanner
+ if (quaggaInitialized) {
+ Quagga.stop();
+ quaggaInitialized = false;
+ startScanner(); // This will pick up the new toggle state from the checkbox
+ }
+ });
+ } else if (quaggaInitialized) {
+ // If getActiveTrack is not available or Quagga is initialized but track isn't, restart scanner.
+ console.log("Flash toggled, getActiveTrack not available or other issue, restarting scanner for change to take effect.");
+ Quagga.stop();
+ quaggaInitialized = false; // Force re-init
+ startScanner(); // This will pick up the new toggle state
+ }
+ // If Quagga is not yet initialized, startScanner() will pick up the state when it's called.
+ });
+ }
};
diff --git a/public/style.css b/public/style.css
new file mode 100644
index 0000000..a9203c9
--- /dev/null
+++ b/public/style.css
@@ -0,0 +1,110 @@
+/* Basic Modal Styling */
+.modal {
+ display: none; /* Hidden by default */
+ position: fixed; /* Stay in place */
+ z-index: 1000; /* Sit on top */
+ left: 0;
+ top: 0;
+ width: 100%; /* Full width */
+ height: 100%; /* Full height */
+ overflow: auto; /* Enable scroll if needed */
+ background-color: rgba(0,0,0,0.4); /* Black w/ opacity (backdrop) */
+}
+
+.modal-content {
+ background-color: #fefefe;
+ margin: 10% auto; /* 10% from the top and centered horizontally */
+ padding: 20px;
+ border: 1px solid #888;
+ width: 80%; /* Could be more or less, depending on screen size */
+ max-width: 700px; /* Max width for larger screens */
+ border-radius: 8px;
+ position: relative; /* For positioning the close button */
+ box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
+}
+
+/* Close Button */
+.close-button {
+ color: #aaa;
+ float: right; /* Position to the top-right */
+ font-size: 28px;
+ font-weight: bold;
+ position: absolute; /* Position relative to modal-content */
+ top: 10px;
+ right: 20px;
+}
+
+.close-button:hover,
+.close-button:focus {
+ color: black;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+/* Style for the book list entries on edit_books.html for better layout */
+#book-list-container ul {
+ list-style-type: none;
+ padding: 0;
+}
+
+#book-list-container li {
+ border: 1px solid #ddd;
+ margin-bottom: 10px;
+ padding: 10px;
+ border-radius: 5px;
+}
+
+.book-entry {
+ display: flex; /* Use flexbox for alignment */
+ align-items: flex-start; /* Align items to the start of the cross axis */
+}
+
+.book-entry img {
+ margin-right: 15px;
+ flex-shrink: 0; /* Prevent image from shrinking */
+}
+
+.book-entry div {
+ flex-grow: 1; /* Allow text content to take up remaining space */
+}
+
+.book-entry button {
+ margin-left: 10px;
+ align-self: center; /* Center button vertically within the flex container */
+}
+
+/* Ensure modal input fields are a bit more organized */
+.modal-content div {
+ margin-bottom: 10px;
+}
+.modal-content label {
+ display: block;
+ margin-bottom: 5px;
+}
+.modal-content input[type="text"],
+.modal-content input[type="number"],
+.modal-content textarea {
+ width: calc(100% - 22px); /* Adjust width to account for padding/border */
+ padding: 8px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-sizing: border-box; /* Include padding and border in the element's total width and height */
+}
+.modal-content textarea {
+ resize: vertical;
+ min-height: 80px;
+}
+.modal-content button {
+ padding: 10px 15px;
+ background-color: #007bff;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+.modal-content button:hover {
+ background-color: #0056b3;
+}
+#modal-search-button {
+ margin-left: 5px;
+}