diff --git a/cmd/serve/api.go b/cmd/serve/api.go index 516748c..cdf3c8f 100644 --- a/cmd/serve/api.go +++ b/cmd/serve/api.go @@ -22,27 +22,14 @@ type AdminRouter struct { ts *tailscale.LocalClient } -type handler struct { - get func() - post func() - put func() - delete func() -} +type handler map[string]func() func (h handler) Handle(w http.ResponseWriter, req *http.Request) { - if req.Method == http.MethodHead && h.get != nil { - h.get() - } else if req.Method == http.MethodGet && h.get != nil { - h.get() - } else if req.Method == http.MethodPost && h.post != nil { - h.post() - } else if req.Method == http.MethodPut && h.put != nil { - h.put() - } else if req.Method == http.MethodDelete && h.delete != nil { - h.delete() - } else { - badMethod(w) + if f, ok := h[req.Method]; ok { + f() + return } + badMethod(w) } func writeJSONerror(w http.ResponseWriter, err string, status int) { @@ -65,11 +52,11 @@ func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/records": handler{ - get: func() { getRecords(router.rcol, w, r) }, + http.MethodGet: func() { getRecords(router.rcol, w, r) }, }.Handle(w, r) case "/api/books": handler{ - get: func() { getBooks(router.lib, w, r) }, + http.MethodGet: func() { getBooks(router.lib, w, r) }, }.Handle(w, r) default: static(router.static).ServeHTTP(w, r) @@ -80,13 +67,13 @@ func (router *AdminRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/whoami": handler{ - get: func() { getWhoAmI(router.ts, w, r) }, + http.MethodGet: func() { getWhoAmI(router.ts, w, r) }, }.Handle(w, r) case "/api/books": handler{ - get: func() { getBooks(router.lib, w, r) }, - post: func() { addBook(router.lib, w, r) }, - delete: func() { deleteBook(router.lib, w, r) }, + http.MethodGet: func() { getBooks(router.lib, w, r) }, + http.MethodPost: func() { addBook(router.lib, w, r) }, + http.MethodDelete: func() { deleteBook(router.lib, w, r) }, }.Handle(w, r) default: static(router.static).ServeHTTP(w, r) diff --git a/frontend/admin/app.js b/frontend/admin/app.js new file mode 100644 index 0000000..d8c2d4c --- /dev/null +++ b/frontend/admin/app.js @@ -0,0 +1,217 @@ +var sortState = { + sortBy: "sortAuthor", + sortOrder: "asc", +}; + +function init() { + fetch("/api/books") + .then((response) => response.json()) + .then((books) => { + // prepare response + books.forEach(apiResponseParsing); + document.getElementById("search").addEventListener("input", (e) => { + renderTable( + search( + books, + e.target.value, + document.getElementById("childrens").checked + ) + ); + }); + document.getElementById("childrens").addEventListener("change", (e) => { + renderTable( + search( + books, + document.getElementById("search").value, + e.target.checked + ) + ); + }); + renderTable( + search(books, "", document.getElementById("childrens").checked) + ); + }); +} + +function renderTable(books, sortField) { + if (sortField) { + if (sortState.sortBy === sortField && sortState.sortOrder === "asc") { + sortState.sortOrder = "desc"; + } else { + sortState.sortOrder = "asc"; + } + sortState.sortBy = sortField; + } + books.sort((one, two) => + (one[sortState.sortBy] + one["sortTitle"]).localeCompare( + two[sortState.sortBy] + two["sortTitle"] + ) + ); + if (sortState.sortOrder === "desc") { + books.reverse(); + } + books.forEach((e, i) => (e.rowNumber = i)); // re-key + + // rendering + var bookElement = document.getElementById("books"); + bookElement.innerHTML = TableTemplate(books); + + var bookCount = document.getElementById("bookCount"); + bookCount.innerHTML = `${books.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( + books[e.currentTarget.id] + ); + }); + }); + // add sorting callbacks + Array.from(bookElement.querySelectorAll("tbody tr th[data-sort-by]")).forEach( + (row) => { + row.addEventListener("click", function (e) { + renderTable(books, e.target.dataset.sortBy); // only add callback when there's a sortBy attribute + }); + } + ); + // 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(books, searchBy, includeChildrensBooks) { + searchBy = searchCleaner(searchBy); + books = 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; + } + ); + return books; +} + +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 BookTemplate({ + "isbn-13": isbn13, + "isbn-10": isbn10, + authors, + coverURL, + description, + format, + notes, + publisher, + series, + signed, + title, + volume, + year, +}) { + return ` +
Title | +Author | +Publisher | +Year | +ISBN | +
---|