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

+ + +
+ +
+ + + + + + + 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 = ` +
+ Cover for ${book.title || 'N/A'} +
+

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; +}