All checks were successful
		
		
	
	ci/woodpecker/push/woodpecker Pipeline was successful
				
			Reviewed-on: #17 Co-authored-by: David Ashby <delta.mu.alpha@gmail.com> Co-committed-by: David Ashby <delta.mu.alpha@gmail.com>
		
			
				
	
	
		
			323 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			323 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| var sortState = {
 | |
|   sortBy: "sortAuthor",
 | |
|   sortOrder: "asc",
 | |
| };
 | |
| 
 | |
| var admin = false;
 | |
| 
 | |
| var books;
 | |
| 
 | |
| function checkAdminMode() {
 | |
|   fetch("/api/mode")
 | |
|     .then((response) => response.json())
 | |
|     .then((resp) => (admin = resp.Admin))
 | |
|     .then(() => {
 | |
|       if (admin) {
 | |
|         var element = document.getElementById("addBook");
 | |
|         element.addEventListener("click", (e) => {
 | |
|           e.preventDefault();
 | |
|           renderAddBookView();
 | |
|         });
 | |
|         element.classList.remove("hidden");
 | |
|       }
 | |
|     });
 | |
| }
 | |
| 
 | |
| function loadBookList() {
 | |
|   fetch("/api/books")
 | |
|     .then((response) => response.json())
 | |
|     .then((list) => {
 | |
|       // prepare response
 | |
|       list.forEach(apiResponseParsing);
 | |
|       books = list;
 | |
|       document.getElementById("search").addEventListener("input", rerender);
 | |
|       document.getElementById("childrens").addEventListener("change", rerender);
 | |
|       rerender();
 | |
|     });
 | |
| }
 | |
| 
 | |
| function rerender() {
 | |
|   var searchValue = document.getElementById("search").value;
 | |
|   var childrens = document.getElementById("childrens").checked;
 | |
|   renderTable(search(searchValue, childrens));
 | |
| }
 | |
| 
 | |
| function init() {
 | |
|   checkAdminMode();
 | |
|   loadBookList();
 | |
| }
 | |
| 
 | |
| function renderAddBookView() {
 | |
|   document.getElementById("current").innerHTML = AddBookTemplate();
 | |
|   document.getElementById("lookup").addEventListener("click", (e) => {
 | |
|     e.preventDefault();
 | |
|     if (document.getElementById("isbn-13").value.length === 13) {
 | |
|       getPossibleBooks(document.getElementById("isbn-13").value);
 | |
|     } else {
 | |
|       console.log("no isbn");
 | |
|     }
 | |
|   });
 | |
|   document.getElementById("save").addEventListener("click", (e) => {
 | |
|     e.preventDefault();
 | |
|     saveBook({
 | |
|       title: document.getElementById("title").value,
 | |
|       authors: document.getElementById("authors").value.split(";"),
 | |
|       sortAuthor: document.getElementById("sortAuthor").value,
 | |
|       "isbn-10": document.getElementById("isbn-10").value,
 | |
|       "isbn-13": document.getElementById("isbn-13").value,
 | |
|       publisher: document.getElementById("publisher").value,
 | |
|       format: document.getElementById("format").value,
 | |
|       genre: document.getElementById("genre").value,
 | |
|       series: document.getElementById("series").value,
 | |
|       volume: document.getElementById("volume").value,
 | |
|       year: document.getElementById("year").value,
 | |
|       coverURL: document.getElementById("coverURL").value,
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| function getPossibleBooks(isbn) {
 | |
|   fetch("/api/query", {
 | |
|     method: "POST",
 | |
|     headers: { "Content-Type": "application/json" },
 | |
|     body: JSON.stringify({ "isbn-13": isbn }),
 | |
|   })
 | |
|     .then((response) => response.json())
 | |
|     .then((json) => {
 | |
|       Object.keys(json).forEach((key) => {
 | |
|         var elem = document.getElementById(key);
 | |
|         if (elem !== null) {
 | |
|           elem.value = json[key];
 | |
|         }
 | |
|       });
 | |
|     });
 | |
| }
 | |
| 
 | |
| function saveBook(book) {
 | |
|   fetch("/api/books", {
 | |
|     method: "POST",
 | |
|     headers: { "Content-Type": "application/json" },
 | |
|     body: JSON.stringify(book),
 | |
|   }).then(() => {
 | |
|     clearAddBookForm();
 | |
|     loadBookList();
 | |
|   });
 | |
| }
 | |
| 
 | |
| function renderTable(bookList, sortField) {
 | |
|   if (sortField) {
 | |
|     sortState.sortOrder =
 | |
|       sortState.sortBy === sortField && sortState.sortOrder === "asc"
 | |
|         ? "desc"
 | |
|         : "asc";
 | |
|     sortState.sortBy = sortField;
 | |
|   }
 | |
|   bookList.sort((one, two) =>
 | |
|     (one[sortState.sortBy] + one["sortTitle"]).localeCompare(
 | |
|       two[sortState.sortBy] + two["sortTitle"]
 | |
|     )
 | |
|   );
 | |
|   if (sortState.sortOrder === "desc") {
 | |
|     bookList.reverse();
 | |
|   }
 | |
|   bookList.forEach((e, i) => (e.rowNumber = i)); // re-key
 | |
| 
 | |
|   // rendering
 | |
|   var bookElement = document.getElementById("books");
 | |
|   bookElement.innerHTML = TableTemplate(bookList);
 | |
| 
 | |
|   document.getElementById("bookCount").innerHTML = `${bookList.length} books`;
 | |
| 
 | |
|   // add listeners for selecting book to view
 | |
|   Array.from(bookElement.querySelectorAll("tbody tr"))
 | |
|     .slice(1) // remove header from Array
 | |
|     .forEach((row) => {
 | |
|       row.addEventListener("click", (e) => {
 | |
|         // add listener to swap current book
 | |
|         document.getElementById("current").innerHTML = BookTemplate(
 | |
|           bookList[e.currentTarget.id]
 | |
|         );
 | |
|       });
 | |
|     });
 | |
|   // add sorting callbacks
 | |
|   Array.from(bookElement.querySelectorAll("tbody tr th[data-sort-by]")).forEach(
 | |
|     (row) => {
 | |
|       row.addEventListener("click", function (e) {
 | |
|         // only add callback when there's a sortBy attribute
 | |
|         renderTable(bookList, e.target.dataset.sortBy);
 | |
|       });
 | |
|     }
 | |
|   );
 | |
|   // mark currently active column
 | |
|   bookElement
 | |
|     .querySelector("tbody tr th[data-sort-by=" + sortState.sortBy + "]")
 | |
|     .classList.add(sortState.sortOrder);
 | |
| }
 | |
| 
 | |
| function apiResponseParsing(book) {
 | |
|   book.sortTitle = titleCleaner(book.title);
 | |
|   if (!book["isbn-10"] && book["isbn-13"]) {
 | |
|     book["isbn-10"] = ISBNfromEAN(book["isbn-13"]);
 | |
|   }
 | |
|   if (!book.coverURL && book["isbn-10"]) {
 | |
|     book.coverURL =
 | |
|       `https://images-na.ssl-images-amazon.com/images/P/` +
 | |
|       book["isbn-10"] +
 | |
|       `.01.LZZ.jpg`;
 | |
|   }
 | |
|   return book;
 | |
| }
 | |
| 
 | |
| function search(searchBy, includeChildrensBooks) {
 | |
|   searchBy = searchCleaner(searchBy);
 | |
|   return books.filter(
 | |
|     ({ title, authors, genre, publisher, series, year, childrens }) => {
 | |
|       var inSearch = true;
 | |
|       if (searchBy !== "") {
 | |
|         inSearch = Object.values({
 | |
|           title,
 | |
|           authors: authors.join(" "),
 | |
|           genre,
 | |
|           publisher,
 | |
|           series,
 | |
|           year,
 | |
|         }).find((field) => searchCleaner(field).indexOf(searchBy) !== -1);
 | |
|       }
 | |
|       if (!includeChildrensBooks) {
 | |
|         return inSearch && !childrens;
 | |
|       }
 | |
|       return inSearch;
 | |
|     }
 | |
|   );
 | |
| }
 | |
| 
 | |
| function titleCleaner(title) {
 | |
|   return title
 | |
|     .replace('"', "")
 | |
|     .replace(":", "")
 | |
|     .replace(/^(An?|The)\s/i, "");
 | |
| }
 | |
| 
 | |
| function searchCleaner(str) {
 | |
|   return str
 | |
|     .toLowerCase()
 | |
|     .replaceAll('"', "")
 | |
|     .replaceAll(":", "")
 | |
|     .replaceAll("'", "")
 | |
|     .replaceAll(" ", "");
 | |
| }
 | |
| 
 | |
| function ISBNfromEAN(EAN) {
 | |
|   ISBN = EAN.slice(3, 12);
 | |
|   var checkdigit =
 | |
|     (11 - (ISBN.split("").reduce((s, n, k) => s + n * (10 - k), 0) % 11)) % 11;
 | |
|   return ISBN + (checkdigit === 10 ? "X" : checkdigit);
 | |
| }
 | |
| 
 | |
| function clearAddBookForm() {
 | |
|   document
 | |
|     .getElementById("newBookForm")
 | |
|     .childNodes.forEach((node) =>
 | |
|       node.nodeName === "LABEL" ? (node.lastChild.value = "") : null
 | |
|     );
 | |
| }
 | |
| 
 | |
| function BookTemplate({
 | |
|   "isbn-13": isbn13,
 | |
|   "isbn-10": isbn10,
 | |
|   authors,
 | |
|   coverURL,
 | |
|   format,
 | |
|   publisher,
 | |
|   series,
 | |
|   signed,
 | |
|   title,
 | |
|   volume,
 | |
|   year,
 | |
| }) {
 | |
|   return `<img ${coverURL ? `src="${coverURL}"` : ``}/>
 | |
|   <div class="bookDetails">
 | |
|     <h1>${title}</h1>
 | |
|     <h2>${authors}</h2>
 | |
|     <span>${[isbn10, isbn13].filter((v) => v != "").join(" / ")}</span><br/>
 | |
|     <span>${publisher}, ${year}</span><br/>
 | |
|     ${
 | |
|       series
 | |
|         ? `<span>${series}${volume ? `, Volume ${volume}` : ""}</span><br/>`
 | |
|         : ""
 | |
|     }
 | |
|     ${signed ? "<span>Signed by the author ✒</span><br/>" : ""}
 | |
|     <span>${format}</span>
 | |
|     ${admin ? `<a href="#">Edit Book</a>` : ""}
 | |
|   </div>`;
 | |
| }
 | |
| 
 | |
| function TableRowTemplate({
 | |
|   "isbn-13": isbn13,
 | |
|   "isbn-10": isbn10,
 | |
|   authors,
 | |
|   publisher,
 | |
|   rowNumber,
 | |
|   signed,
 | |
|   title,
 | |
|   year,
 | |
| }) {
 | |
|   return `<tr class="tRow" id="${rowNumber}">
 | |
|     <td class="title">
 | |
|       ${title} ${
 | |
|     signed ? '<span class="signed" title="Signed by the author" >✒</span>' : ""
 | |
|   }
 | |
|     </td>
 | |
|     <td class="author">${authors}</td>
 | |
|     <td class="publisher">${publisher}</td>
 | |
|     <td class="year">${year}</td>
 | |
|     <td class="isbn">${isbn13 ? isbn13 : isbn10}</td>
 | |
|   </tr>`;
 | |
| }
 | |
| 
 | |
| function TableTemplate(books) {
 | |
|   return `<table class="bookTable">
 | |
|     <tr>
 | |
|       <th data-sort-by="sortTitle" class="tHeader title">Title</th>
 | |
|       <th data-sort-by="sortAuthor" class="tHeader author">Author</th>
 | |
|       <th data-sort-by="publisher" class="tHeader publisher">Publisher</th>
 | |
|       <th data-sort-by="year" class="tHeader year">Year</th>
 | |
|       <th class="tHeader isbn">ISBN</th>
 | |
|     </tr>${books.reduce((acc, book) => {
 | |
|       return acc.concat(TableRowTemplate(book));
 | |
|     }, "")} </table>`;
 | |
| }
 | |
| 
 | |
| function AddBookTemplate() {
 | |
|   return `<div class="addBookView">
 | |
|     <div id="newBookForm">
 | |
|       ${[
 | |
|         { name: "Title", id: "title", type: "text" },
 | |
|         { name: "Authors", id: "authors", type: "text" },
 | |
|         { name: "SortAuthor", id: "sortAuthor", type: "text" },
 | |
|         { name: "ISBN10", id: "isbn-10", type: "text" },
 | |
|         { name: "ISBN13", id: "isbn-13", type: "text" },
 | |
|         { name: "Publisher", id: "publisher", type: "text" },
 | |
|         { name: "Format", id: "format", type: "text" },
 | |
|         { name: "Genre", id: "genre", type: "text" },
 | |
|         { name: "Series", id: "series", type: "text" },
 | |
|         { name: "Volume", id: "volume", type: "text" },
 | |
|         { name: "Year", id: "year", type: "text" },
 | |
|         { name: "CoverURL", id: "coverURL", type: "text" },
 | |
|         { name: "Signed", id: "signed", type: "checkbox" },
 | |
|         { name: "Childrens", id: "childrens", type: "checkbox" },
 | |
|       ].reduce((acc, field) => {
 | |
|         return acc.concat(
 | |
|           `<label>${field.name} <input
 | |
|               type="${field.type}"
 | |
|               name="${field.name.toLowerCase()}"
 | |
|               id="${field.id}"
 | |
|             /></label><br/>`
 | |
|         );
 | |
|       }, "")}
 | |
|       <input id="lookup" type="submit" value="look up">
 | |
|       <input id="save" type="submit" value="save">
 | |
|     </div>
 | |
|   </div>`;
 | |
| }
 |