diff --git a/src/index.ts b/src/index.ts index 6fc362c..e9d8f82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import * as hbs from 'express-handlebars'; import * as session from 'express-session'; import * as bodyParser from 'body-parser'; import * as routes from './routes'; -import { renderPage, Request, Response } from './routes/util'; +import { renderPage, Request, Response, NextFunction } from './routes/util'; const debug = Boolean(Number(process.env.DEBUG)); const port = Number(process.env.PORT); @@ -15,7 +15,7 @@ var app = express(); // Disable caching for authentication purposes app.set('etag', false); -app.use((req, res, next) => { +app.use((req: Request, res: Response, next: NextFunction) => { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private'); next(); }); diff --git a/src/routes/admin.ts b/src/routes/admin.ts index 3353dad..c5bdd2e 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -207,3 +207,19 @@ router.get('/users', adminAuth, (req: Request, res: Response) => { renderPage(req, res, 'admin-users', { users: users, orderBy: orderBy, orderDirection: orderDirection }); }); }); + +// Admin books page +router.get('/books', adminAuth, (req: Request, res: Response) => { + var orderBy = req.query.orderBy as string || 'listedTimestamp'; + var orderDirection = req.query.orderDirection as string || 'ASC'; + services.AdminService.getBooks(orderBy, orderDirection, (books) => { + renderPage(req, res, 'admin-books', { books: books, orderBy: orderBy, orderDirection: orderDirection }); + }); +}); + +// Admin rows page +router.get('/rows', adminAuth, (req: Request, res: Response) => { + services.AdminService.getRowCount((tables) => { + renderPage(req, res, 'admin-rows', { tables: tables }); + }); +}); diff --git a/src/routes/util.ts b/src/routes/util.ts index 944a3cd..b0fb7b4 100644 --- a/src/routes/util.ts +++ b/src/routes/util.ts @@ -185,11 +185,11 @@ export function validBook(form: BookForm, callback: (success: boolean, error: st callback(false, 'Please enter a description of at most 1024 characters.'); } else { // Check ISBN10 - if (ISBN10.length > 0 && !validISBN(ISBN10)) { + if (ISBN10.length > 0 && (ISBN10.length !== 10 || !validISBN(ISBN10))) { callback(false, 'Please enter a valid ISBN-10.'); } else { // Check ISBN13 - if (ISBN13.length > 0 && !validISBN(ISBN13)) { + if (ISBN13.length > 0 && (ISBN13.length !== 13 || !validISBN(ISBN13))) { callback(false, 'Please enter a valid ISBN-13.'); } else { callback(true, null, { diff --git a/src/services/admin.ts b/src/services/admin.ts index ca8a33b..04dfb99 100644 --- a/src/services/admin.ts +++ b/src/services/admin.ts @@ -35,7 +35,8 @@ export module AdminService { query_to_xml(format('SELECT COUNT(*) AS cnt FROM %I.%I', table_schema, table_name), false, true, '') AS xml_count FROM information_schema.tables WHERE table_schema = 'public' - ) t; + ) t + ORDER BY rows DESC; `; mainDB.execute(sql, [], (rows) => { if (callback) callback(rows); @@ -98,4 +99,22 @@ export module AdminService { }); } + // Get relevant information on all books + export function getBooks(orderBy: string, orderDirection: string, callback?: rowsCallback) { + var sql = ` + SELECT + bookId, title, author, + CONCAT(NBUser.firstname, ' ', NBUser.lastname) AS listedBy, + Department.name as department, + courseNumber, price, listedTimestamp + FROM Book + JOIN NBUser ON Book.userId = NBUser.id + JOIN Department ON Book.departmentId = Department.id + ORDER BY ${orderBy} ${orderDirection}; + `; + mainDB.execute(sql, [], (rows) => { + if (callback) callback(rows); + }); + } + } diff --git a/src/static/css/main.css b/src/static/css/main.css index 7173e3d..e3ce231 100644 --- a/src/static/css/main.css +++ b/src/static/css/main.css @@ -152,12 +152,12 @@ footer .footer-list-link { /* Index Page */ #index .card { - margin: 0 auto; border: none; padding: 0px 20px; } -#index .card-link { +#index .full-card-link { + margin: 0 auto; text-decoration: none; } @@ -481,3 +481,8 @@ footer .footer-list-link { margin: 0; border-radius: 0; } + +#rows-chart-container { + max-width: min(90vw, 70vh); + margin: 24px auto; +} diff --git a/src/static/js/admin-rows.js b/src/static/js/admin-rows.js new file mode 100644 index 0000000..895299f --- /dev/null +++ b/src/static/js/admin-rows.js @@ -0,0 +1,52 @@ +function selectColor(number) { + var hue = Math.round(number * 137.508) % 360; // golden angle approximation + return `hsl(${hue}, 100%, 50%)`; +} + +function generateColors(numColors) { + var colors = []; + for (let i = 0; i < numColors; i++) { + colors.push(selectColor(i)); + } + return colors; +} + +function getRowsData() { + var tables = []; + var rows = []; + + $('#rows-table').find($('tr')).find($('.rows-table-table')) + .each(function() { + tables.push($(this).text()); + }); + + $('#rows-table').find($('tr')).find($('.rows-table-rows')) + .each(function() { + rows.push($(this).text()); + }); + + return { tables, rows }; +} + +function createChart() { + var img = new Image(); + img.src = 'https://www.norsebooks.com/img/favicon.png'; + + var rowsData = getRowsData(); + var colors = generateColors(rowsData.tables.length); + var ctx = document.getElementById('rows-chart').getContext('2d'); + var chart = new Chart(ctx, { + type: 'doughnut', + data: { + labels: rowsData.tables, + datasets: [{ + data: rowsData.rows, + backgroundColor: colors + }] + } + }); +} + +$(() => { + createChart(); +}); diff --git a/src/static/js/admin-users.js b/src/static/js/column-sort.js similarity index 100% rename from src/static/js/admin-users.js rename to src/static/js/column-sort.js diff --git a/src/static/js/index.js b/src/static/js/index.js index 73c638d..4ff24a8 100644 --- a/src/static/js/index.js +++ b/src/static/js/index.js @@ -5,18 +5,32 @@ function getLastBookId() { else return bookCard.getElementsByTagName('a')[0].href.slice(-4); } +// Remove the last book on the page function deleteLastBook() { var bookCard = document.getElementById('index').lastElementChild; bookCard.parentNode.removeChild(bookCard); } +// Transform book image URLs to load smaller images +function smallerImageURL(imageUrl) { + const imgStart = 'https://res.cloudinary.com/norsebooks/image/upload'; + const imgWidth = 300; + if (imageUrl.startsWith(imgStart)) { + var imgEnd = imageUrl.slice(imgStart.length + 1); + return `${imgStart}/w_${imgWidth}/${imgEnd}`; + } else { + return imageUrl; + } +} + // Add a book to the end of the page function addBook(book) { var courseNumber = book.coursenumber ? ' ' + book.coursenumber : ''; - //
+ // var newBookLink = document.createElement('a'); - newBookLink.classList.add('card-link'); + newBookLink.classList.add('full-card-link'); newBookLink.href = `/book/${book.bookid}`; + //
var newBook = document.createElement('div'); newBook.classList.add('card-container', 'col-12', 'col-md-6', 'col-lg-4', 'mb-4'); //
@@ -28,7 +42,7 @@ function addBook(book) { newCard.appendChild(imgLink); // ... var newImg = document.createElement('img'); - newImg.src = book.imageurl; + newImg.src = smallerImageURL(book.imageurl); newImg.classList.add('card-img-top', 'thumbnail', 'p-1', 'pt-3'); newImg.alt = '...'; imgLink.appendChild(newImg); diff --git a/src/static/js/new-book.js b/src/static/js/new-book.js index 8ecc4c8..9fd7d89 100644 --- a/src/static/js/new-book.js +++ b/src/static/js/new-book.js @@ -1,4 +1,6 @@ -$('#search-google-api').on('keyup', function(){ +const apiKey = 'AIzaSyCZb0ZbkRKgq06SprrrF3DNbxCZdjp8TP0'; + +$('#search-google-api').on('keyup', function() { var searchFieldValue = document.getElementById('search-google-api').value; searchFieldValue = searchFieldValue.split(' ').join('+'); @@ -6,7 +8,7 @@ $('#search-google-api').on('keyup', function(){ var resultListItems = document.querySelectorAll('.search-result-link'); - var url = 'https://www.googleapis.com/books/v1/volumes?key=' + 'AIzaSyCZb0ZbkRKgq06SprrrF3DNbxCZdjp8TP0' + '&q=' + searchFieldValue + '&printType=books'; + var url = `https://www.googleapis.com/books/v1/volumes?key=${apiKey}&q=${searchFieldValue}&printType=books`; var xhr = new XMLHttpRequest(); if (searchFieldValue !== '') { xhr.open('GET', url); @@ -19,7 +21,6 @@ $('#search-google-api').on('keyup', function(){ var resultArray = responseText.items; for (var i = 0; i < 5; i++) { resultListItems[i].setAttribute('onclick', `populateBookInfo('${resultArray[i].id}')`); - console.log(responseText); var titleSpan = document.createElement('span'); titleSpan.classList.add('title'); titleSpan.innerText = resultArray[i].volumeInfo.title; @@ -40,10 +41,10 @@ $('#search-google-api').on('keyup', function(){ function populateBookInfo(volumeId) { document.getElementById('result-list-container').style.display = 'none'; - url = `https://www.googleapis.com/books/v1/volumes/${volumeId}?key=AIzaSyCZb0ZbkRKgq06SprrrF3DNbxCZdjp8TP0`; + url = `https://www.googleapis.com/books/v1/volumes/${volumeId}?key=${apiKey}`; var xhr = new XMLHttpRequest(); xhr.open('GET', url); - xhr.send() + xhr.send(); xhr.onreadystatechange = (e) => { if (xhr.status === 200) { var responseText = JSON.parse(xhr.responseText); @@ -52,15 +53,13 @@ function populateBookInfo(volumeId) { var isbn10Field = document.querySelector('#ISBN10'); var isbn13Field = document.querySelector('#ISBN13'); titleField.value = responseText.volumeInfo.title; - titleField.classList.add("autofill-success") + titleField.classList.add('autofill-success'); authorField.value = responseText.volumeInfo.authors[0]; - authorField.classList.add("autofill-success") + authorField.classList.add('autofill-success'); isbn10Field.value = responseText.volumeInfo.industryIdentifiers[0].identifier; isbn13Field.value = responseText.volumeInfo.industryIdentifiers[1].identifier; - isbn10Field.classList.add("autofill-success") - isbn13Field.classList.add("autofill-success") - console.log(responseText); - // MAKE FIELDS GREEN! + isbn10Field.classList.add('autofill-success'); + isbn13Field.classList.add('autofill-success'); } } -} \ No newline at end of file +} diff --git a/src/views/admin-books.html b/src/views/admin-books.html new file mode 100644 index 0000000..9d7c924 --- /dev/null +++ b/src/views/admin-books.html @@ -0,0 +1,33 @@ + +
+

Books

+

This page contains relevant information on books. To sort by a column, click on the column header. Click here to return to the admin page.

+ + + + + + + + + + + + + + {{#each books}} + + + + + + + + + + {{/each}} + +
TitleAuthorListed byDepartmentCourse numberPriceListed
{{this.title}}{{this.author}}{{this.listedby}}{{this.department}}{{this.coursenumber}}${{this.price}}{{this.listedtimestamp}}
+ + +
diff --git a/src/views/admin-rows.html b/src/views/admin-rows.html new file mode 100644 index 0000000..ef8787e --- /dev/null +++ b/src/views/admin-rows.html @@ -0,0 +1,25 @@ + + +
+

Database Rows

+

This page shows how the database is divided. See the chart or the table below to see how many rows each table uses. Click here to return to the admin page.

+
+ +
+ + + + + + + + + {{#each tables}} + + + + + {{/each}} + +
TableRows
{{this.table}}{{this.rows}}
+
diff --git a/src/views/admin-users.html b/src/views/admin-users.html index e77e6a4..23b99e4 100644 --- a/src/views/admin-users.html +++ b/src/views/admin-users.html @@ -1,4 +1,4 @@ - +

Users

This page contains relevant information on users. To sort by a column, click on the column header. Click here to return to the admin page.

diff --git a/src/views/admin.html b/src/views/admin.html index 7fb3eda..d948340 100644 --- a/src/views/admin.html +++ b/src/views/admin.html @@ -13,8 +13,10 @@

Stats

-
Books
-
...
+ +
Books
+
...
+
Sold
@@ -33,8 +35,10 @@

Stats

...
-
Rows
-
...
+ +
Rows
+
...
+
Capacity