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