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:
74
public/books-with-images.html
Normal file
74
public/books-with-images.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
298
public/script.js
298
public/script.js
@@ -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('');
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user