Refactor book management system:

- Updated .gitignore to exclude node_modules directory.
- Enhanced index.js with new endpoints for book fetching and checkout requests, integrating nodemailer and express-rate-limit for email notifications and request management.
- Added functionality to confirm book presence in the library and improved error handling for external book sources.
- Updated package.json and package-lock.json to include new dependencies (nodemailer, express-rate-limit) and their respective versions.
- Modified public HTML and JavaScript files to support new features, including a confirm mode for book scanning and improved UI elements.
- Updated styles for better user experience in the library interface.
This commit is contained in:
2024-12-11 09:41:22 -05:00
parent 7d788cd094
commit 3de9f3d8ee
15 changed files with 2203 additions and 647 deletions

View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<title>Books with Images</title>
<style>
body {
font-family: Arial, sans-serif;
}
.book-item {
margin-bottom: 20px;
}
.book-item img {
max-width: 200px;
height: auto;
display: block;
cursor: pointer; /* Add cursor style to indicate clickable */
}
</style>
</head>
<body>
<h1>Books with Images</h1>
<div id="books-list"></div>
<script>
fetch('/api/books-with-images')
.then(response => response.json())
.then(books => {
const booksList = document.getElementById('books-list');
books.forEach(book => {
const bookDiv = document.createElement('div');
bookDiv.className = 'book-item';
const title = document.createElement('h2');
title.textContent = book.title;
bookDiv.appendChild(title);
const isbn = document.createElement('p');
isbn.textContent = 'ISBN: ' + book.isbn;
bookDiv.appendChild(isbn);
// Aurthor
const author = document.createElement('p');
author.textContent = 'Author: ' + book.authors;
bookDiv.appendChild(author);
let imgSrc = book.cover_large || book.cover_medium || book.cover_small;
if (imgSrc) {
const img = document.createElement('img');
img.src = imgSrc;
img.alt = book.title + ' cover';
img.addEventListener('click', () => {
img.style.display = 'none'; // Hide the clicked image
// Hide the title and ISBN as well
title.style.display = 'none';
isbn.style.display = 'none';
});
bookDiv.appendChild(img);
} else {
const noImg = document.createElement('p');
noImg.textContent = 'No cover image available.';
bookDiv.appendChild(noImg);
}
booksList.appendChild(bookDiv);
});
})
.catch(error => {
console.error('Error fetching books:', error);
const booksList = document.getElementById('books-list');
booksList.textContent = 'Failed to load books.';
});
</script>
</body>
</html>

View File

@@ -11,6 +11,9 @@
<div>
<label for="camera-select">Select Camera:</label>
<select id="camera-select"></select>
<!-- Confirm mode toggle -->
<input type="checkbox" id="confirm-mode">
<label for="confirm-mode">Confirm mode</label>s
</div>
<div id="interactive" class="viewport"></div>
<div>

View File

@@ -3,46 +3,23 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Book Library</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
}
#search-bar {
width: 100%;
padding: 10px;
font-size: 16px;
margin-top: 20px;
border: 1px solid #ddd;
}
</style>
<title>Ramsey Library</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Book Library</h1>
<h1>Ramsey Library</h1>
<input type="text" id="search-bar" placeholder="Search by title, author, publisher, etc.">
<table id="book-table">
<thead>
<tr>
<th>Title</th>
<th>Authors</th>
<th>Publisher</th>
<th>Published Date</th>
<th>ISBN</th>
<th onclick="sortTable(0)">Title</th>
<th onclick="sortTable(1)">Authors</th>
<th onclick="sortTable(2)">Publisher</th>
<th onclick="sortTable(3)">Published Date</th>
<th onclick="sortTable(4)">ISBN</th>
<th onclick="sortTable(5)">Status</th>
<th>Checkout</th>
</tr>
</thead>
<tbody id="book-table-body">
@@ -52,7 +29,7 @@
<script>
// Fetch all books when the page loads
window.onload = fetchBooks;
window.onload = fetchBooks('');
document.getElementById('search-bar').addEventListener('input', function() {
fetchBooks(this.value);
@@ -82,16 +59,89 @@
books.forEach(book => {
const row = document.createElement('tr');
const isDisabled = book.status === 'Checked Out' ? 'disabled' : '';
row.innerHTML = `
<td>${book.title || ''}</td>
<td><a href="/book/${book.isbn}" target="_blank">${book.title || ''}</a></td>
<td>${book.authors ? book.authors : ''}</td>
<td>${book.publisher || ''}</td>
<td>${book.publish_date || ''}</td>
<td>${book.publishers || ''}</td>
<td>${book.publishedDate || ''}</td>
<td>${book.isbn || ''}</td>
<td>${book.status || ''}</td>
<td><button id="checkout-btn-${book.isbn}" onclick="requestCheckout('${book.isbn}')" ${isDisabled}>Checkout</button></td>
`;
tbody.appendChild(row);
});
}
// Utility function to get a cookie by name
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
// Utility function to set a cookie
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = `expires=${date.toUTCString()}`;
document.cookie = `${name}=${value}; ${expires}; path=/`;
}
// Client-side function to request checkout
function requestCheckout(isbn) {
let email = getCookie('lastEmail') || prompt('Please enter your email:');
let name = getCookie('lastName') || prompt('Please enter your name:');
if (!email || !name) {
alert('Email and name are required to proceed with checkout.');
return;
}
// Store the email and name in cookies for future use
setCookie('lastEmail', email, 7); // Store for 7 days
setCookie('lastName', name, 7); // Store for 7 days
fetch(`/api/checkout/${isbn}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, name })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Checkout request sent successfully.');
} else {
alert(data.error || 'Failed to send checkout request.');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while sending the checkout request.');
});
}
function sortTable(columnIndex) {
const table = document.getElementById('book-table');
const rows = Array.from(table.rows).slice(1);
const isAscending = table.getAttribute('data-sort-order') === 'asc';
const direction = isAscending ? 1 : -1;
rows.sort((a, b) => {
const aText = a.cells[columnIndex].innerText.toLowerCase();
const bText = b.cells[columnIndex].innerText.toLowerCase();
if (aText < bText) return -1 * direction;
if (aText > bText) return 1 * direction;
return 0;
});
rows.forEach(row => table.tBodies[0].appendChild(row));
table.setAttribute('data-sort-order', isAscending ? 'desc' : 'asc');
}
</script>
</body>
</html>

View File

@@ -1,14 +1,6 @@
let selectedDeviceId;
let isScanning = false; // Flag to prevent multiple scans at the same time
async function testCameraAccess() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
console.log('Camera access granted', stream);
} catch (error) {
console.error('Error accessing camera:', error);
}
}
let quaggaInitialized = false; // Flag to check if Quagga has been initialized
// Get available video input devices (cameras)
async function getCameras() {
@@ -18,23 +10,34 @@ async function getCameras() {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
// Update selectedDeviceId when user selects a camera
cameraSelect.addEventListener('change', function() {
selectedDeviceId = this.value;
startScanner();
});
if (videoDevices.length > 0) {
// Clear any existing options
cameraSelect.innerHTML = '';
videoDevices.forEach((device, index) => {
const option = document.createElement('option');
option.value = device.deviceId;
option.text = device.label || `Camera ${index + 1}`;
cameraSelect.appendChild(option);
});
// Set selectedDeviceId to the first camera by default
selectedDeviceId = cameraSelect.value;
startScanner(); // Start the scanner with the default camera
} else {
console.log("No video devices found.");
cameraSelect.innerHTML = '<option>No cameras found</option>';
}
// Update selectedDeviceId when user selects a camera
cameraSelect.addEventListener('change', function() {
selectedDeviceId = this.value;
// Stop Quagga and re-initialize with the new deviceId
if (quaggaInitialized) {
Quagga.stop();
quaggaInitialized = false;
}
startScanner();
});
} catch (error) {
console.error("Error accessing the camera or enumerating devices:", error);
document.getElementById('camera-select').innerHTML = '<option>' + error + '</option>';
@@ -46,51 +49,56 @@ function startScanner() {
alert('No camera selected or available.');
return;
}
// TODO: Limit the field of view to a smaller area for faster detection
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
target: document.querySelector('#interactive'),
constraints: {
deviceId: selectedDeviceId,
facingMode: "environment", // Default to rear camera,
advanced: [{torch: true}]
},
},
decoder: {
readers: ["ean_reader", "ean_8_reader"]
}
}, function (err) {
if (err) {
console.log(err);
return;
}
console.log("Initialization finished. Ready to start");
if (quaggaInitialized) {
Quagga.start();
} else {
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
target: document.querySelector('#interactive'),
constraints: {
deviceId: selectedDeviceId,
facingMode: "environment",
// Remove advanced constraints if not needed
},
},
decoder: {
readers: ["ean_reader", "ean_8_reader"]
}
}, function (err) {
if (err) {
console.log(err);
return;
}
console.log("Initialization finished. Ready to start");
Quagga.start();
quaggaInitialized = true;
// TODO: Add a button to enable/disable torch
Quagga.CameraAccess.enableTorch();
// Set up the onDetected handler
Quagga.onDetected(processBarcode);
});
}
}
// TODO: Add a button to enable/disable the "locate" functionality
});
async function processBarcode(data) {
if (isScanning) return; // Prevent further scans while a scan is being processed
isScanning = true; // Set the scanning flag
Quagga.onDetected(async function (data) {
if (isScanning) return; // Prevent further scans while a scan is being processed
isScanning = true; // Set the scanning flag
const isbn = data.codeResult.code;
console.log("Detected ISBN:", isbn);
Quagga.stop(); // Stop the scanner once an ISBN is detected
const isbn = data.codeResult.code;
console.log("Detected ISBN:", isbn);
Quagga.stop(); // Stop the scanner once an ISBN is detected
// TODO: Validate the ISBN before fetching book details
// Use a library like barcode-validator to validate the ISBN
// Fetch book details
if (document.getElementById('confirm-mode').checked) {
// Confirm book in library
await confirmBookInLibrary(isbn);
} else {
// Normal flow
await fetchBookInfo(isbn);
}
isScanning = false; // Reset the scanning flag once processing is done
});
isScanning = false; // Reset the scanning flag once processing is done
}
async function fetchBookInfo(isbn) {
@@ -98,10 +106,6 @@ async function fetchBookInfo(isbn) {
const response = await fetch(`/book/${isbn}`);
const bookData = await response.json();
// TODO: If bookData value of "source" is "local", then the book data is already in the database
// TODO: Display the book info and ask if they would like to checkout the book
// TODO: Store scanned ISBN in a different field to check against the one we get from the API
if (bookData.title) {
bookData.isbn2 = isbn; // Add the ISBN to the book data
promptUserWithBook(bookData);
@@ -136,71 +140,6 @@ function promptUserWithBook(bookData) {
}
}
// TODO: Function to prompt user for physical location of the book
// Should pull from the database and allow the user to select or add a location
// Add an event listener for the search button
document.getElementById('search-title').addEventListener('click', searchByTitle);
async function searchByTitle() {
const title = document.getElementById('title-input').value;
if (!title) {
alert('Please enter a book title to search.');
return;
}
try {
const response = await fetch(`/search-title?title=${encodeURIComponent(title)}`);
const data = await response.json();
if (data.results && data.results.length > 0) {
displaySearchResults(data.results);
} else {
alert('No books found with that title.');
}
} catch (error) {
console.error('Error searching for book by title:', error);
alert('An error occurred while searching for the book.');
}
}
function displaySearchResults(results) {
// Display the search results and allow the user to select one
const bookInfoDiv = document.getElementById('book-info');
bookInfoDiv.innerHTML = ''; // Clear previous results
results.forEach((book, index) => {
const bookElement = document.createElement('div');
bookElement.innerHTML = `
<p><strong>Title:</strong> ${book.title}</p>
<p><strong>Author(s):</strong> ${book.authors.join(', ')}</p>
<p><strong>Published:</strong> ${book.publish_date || 'N/A'}</p>
<p><strong>ISBN:</strong> ${book.isbn}</p>
<p><strong>Publisher:</strong> ${book.publisher || 'N/A'}</p>
<button onclick="selectBook(${index})">Select this book</button>
`;
bookInfoDiv.appendChild(bookElement);
});
// Store the results so that we can reference them when the user makes a selection
window.searchResults = results;
}
function selectBook(index) {
const selectedBook = window.searchResults[index];
console.log('Selected book:', selectedBook);
// You can now fetch more details using the unique key if needed, or directly store this in your database
const isbn = selectedBook.isbn;
if (isbn) {
fetchBookInfo(isbn);
} else {
promptUserWithBook(selectedBook); // You might have to adapt this if there's no ISBN
}
}
async function storeBookInDatabase(bookData) {
try {
const response = await fetch('/store-book', {
@@ -241,9 +180,118 @@ async function storeBookInDatabase(bookData) {
}
}
async function confirmBookInLibrary(isbn) {
try {
const response = await fetch(`/book/confirm/${isbn}`);
const bookData = await response.json();
// Start the scanner when the start button is clicked
//document.getElementById('start-scanner').addEventListener('click', startScanner);
if (bookData.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 message = `Title: ${title}\nAuthor(s): ${authors}\nDescription: ${description}\n\nBook found in the library!`;
alert(message);
} else {
alert('Book not found in the library.');
}
} catch (error) {
console.error('Error confirming book in library:', error);
alert('An error occurred while confirming the book in the library.');
} finally {
startScanner(); // Restart the scanner after processing
}
}
// Add an event listener for the search button
document.getElementById('search-title').addEventListener('click', function() {
const title = document.getElementById('title-input').value;
searchByTitle(title);
});
async function searchByTitle(title = '') {
if (!title) {
alert('Please enter a book title to search.');
return;
}
try {
const response = await fetch(`/search-title?title=${encodeURIComponent(title)}`);
const data = await response.json();
if (data.results && data.results.length > 0) {
displaySearchResults(data.results);
} else {
alert('No books found with that title.');
}
} catch (error) {
console.error('Error searching for book by title:', error);
alert('An error occurred while searching for the book.');
}
}
function displaySearchResults(results) {
// Display the search results and allow the user to select one
const bookInfoDiv = document.getElementById('book-info');
bookInfoDiv.innerHTML = ''; // Clear previous results
results.forEach((book, index) => {
const bookElement = document.createElement('div');
bookElement.innerHTML = `
<p><strong>Title:</strong> ${book.title}</p>
<p><strong>Author(s):</strong> ${book.authors.join(', ')}</p>
<p><strong>Published:</strong> ${book.publishDate || 'N/A'}</p>
<p><strong>ISBN:</strong> ${book.isbn}</p>
<p><strong>Publisher:</strong> ${book.publishers || 'N/A'}</p>
<button onclick="selectBook(${index})">Select this book</button>
`;
bookInfoDiv.appendChild(bookElement);
});
// Store the results so that we can reference them when the user makes a selection
window.searchResults = results;
}
// Client-side function to request checkout
function requestCheckout(isbn) {
fetch(`/api/checkout/${isbn}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Checkout request sent successfully.');
} else {
alert('Failed to send checkout request.');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while sending the checkout request.');
});
}
function selectBook(index) {
const selectedBook = window.searchResults[index];
console.log('Selected book:', selectedBook);
// You can now fetch more details using the unique key if needed, or directly store this in your database
const isbn = selectedBook.isbn;
if (isbn) {
fetchBookInfo(isbn);
} else {
promptUserWithBook(selectedBook); // You might have to adapt this if there's no ISBN
}
}
// Get cameras on page load
window.onload = getCameras;
window.onload = function() {
getCameras();
searchByTitle('');
};

View File

@@ -3,6 +3,8 @@
width: 100%;
height: 400px;
overflow: hidden;
border: 2px solid #000;
background-color: #c0c0c0;
}
canvas.drawing, canvas.drawingBuffer {
@@ -13,18 +15,109 @@ canvas.drawing, canvas.drawingBuffer {
#book-info {
margin-top: 20px;
font-family: 'Courier New', Courier, monospace;
background-color: #f0f0f0;
border: 1px solid #000;
padding: 10px;
}
#prompt {
display: none;
margin-top: 20px;
font-family: 'Courier New', Courier, monospace;
background-color: #f0f0f0;
border: 1px solid #000;
padding: 10px;
}
#prompt-message {
font-weight: bold;
font-family: 'Courier New', Courier, monospace;
color: #800000;
}
#book-title, #book-author, #book-desc {
margin-top: 10px;
font-size: 16px;
font-family: 'Courier New', Courier, monospace;
color: #800000;
}
body {
font-family: 'Courier New', Courier, monospace;
background-color: #ddd4b0;
color: #000;
margin: 0;
padding: 0;
}
h1 {
text-align: center;
color: #800000;
font-size: 24px;
margin-top: 20px;
}
#search-bar {
display: block;
margin: 20px auto;
width: 80%;
padding: 10px;
font-size: 16px;
border: 2px solid #000;
background-color: #f0f0f0;
}
table {
width: 90%;
margin: 20px auto;
border-collapse: collapse;
background-color: #fff;
}
th, td {
border: 1px solid #000;
padding: 10px;
text-align: left;
font-size: 14px;
}
th {
background-color: #c0c0c0;
cursor: pointer;
}
th:hover {
background-color: #a0a0a0;
}
a {
color: #800000;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
#book-table-body tr:nth-child(even) {
background-color: #f0f0f0;
}
#book-table-body tr:nth-child(odd) {
background-color: #e0e0e0;
}
#book-table-body tr:hover {
background-color: #d0d0d0;
}
#book-info, #prompt, #book-title, #book-author, #book-desc, th, td, body {
font-size: 18px;
font-weight: bold;
}
#search-bar:focus {
outline-color: #800000;
outline-style: solid;
outline-width: 2px;
}