David Ashby
315fb4e9d2
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>`;
|
|
}
|