management server listener #17
@@ -14,18 +14,14 @@ type Router struct {
 | 
				
			|||||||
	static  fs.FS
 | 
						static  fs.FS
 | 
				
			||||||
	lib     Library
 | 
						lib     Library
 | 
				
			||||||
	rcol    RecordCollection
 | 
						rcol    RecordCollection
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type AdminRouter struct {
 | 
					 | 
				
			||||||
	static fs.FS
 | 
					 | 
				
			||||||
	lib    Library
 | 
					 | 
				
			||||||
	ts      *tailscale.LocalClient
 | 
						ts      *tailscale.LocalClient
 | 
				
			||||||
 | 
						isAdmin bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type handler map[string]func()
 | 
					type path map[string]func()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (h handler) Handle(w http.ResponseWriter, req *http.Request) {
 | 
					func (h path) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	if f, ok := h[req.Method]; ok {
 | 
						if f, ok := h[r.Method]; ok {
 | 
				
			||||||
		f()
 | 
							f()
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -50,31 +46,33 @@ func writeJSON(w http.ResponseWriter, b any, status int) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
					func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	switch r.URL.Path {
 | 
						switch r.URL.Path {
 | 
				
			||||||
	case "/api/records":
 | 
						case "/api/mode":
 | 
				
			||||||
		handler{
 | 
							path{
 | 
				
			||||||
			http.MethodGet: func() { getRecords(router.rcol, w, r) },
 | 
								http.MethodGet: func() {
 | 
				
			||||||
		}.Handle(w, r)
 | 
									writeJSON(w, struct{ Admin bool }{Admin: router.isAdmin}, http.StatusOK)
 | 
				
			||||||
	case "/api/books":
 | 
								},
 | 
				
			||||||
		handler{
 | 
							}.ServeHTTP(w, r)
 | 
				
			||||||
			http.MethodGet: func() { getBooks(router.lib, w, r) },
 | 
					 | 
				
			||||||
		}.Handle(w, r)
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		static(router.static).ServeHTTP(w, r)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (router *AdminRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
					 | 
				
			||||||
	switch r.URL.Path {
 | 
					 | 
				
			||||||
	case "/api/whoami":
 | 
						case "/api/whoami":
 | 
				
			||||||
		handler{
 | 
							if !router.isAdmin {
 | 
				
			||||||
 | 
								http.NotFoundHandler().ServeHTTP(w, r)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							path{
 | 
				
			||||||
			http.MethodGet: func() { getWhoAmI(router.ts, w, r) },
 | 
								http.MethodGet: func() { getWhoAmI(router.ts, w, r) },
 | 
				
			||||||
		}.Handle(w, r)
 | 
							}.ServeHTTP(w, r)
 | 
				
			||||||
 | 
						case "/api/records":
 | 
				
			||||||
 | 
							path{
 | 
				
			||||||
 | 
								http.MethodGet: func() { getRecords(router.rcol, w, r) },
 | 
				
			||||||
 | 
							}.ServeHTTP(w, r)
 | 
				
			||||||
	case "/api/books":
 | 
						case "/api/books":
 | 
				
			||||||
		handler{
 | 
							p := path{
 | 
				
			||||||
			http.MethodGet: func() { getBooks(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) },
 | 
							if router.isAdmin {
 | 
				
			||||||
		}.Handle(w, r)
 | 
								p[http.MethodPost] = func() { addBook(router.lib, w, r) }
 | 
				
			||||||
 | 
								p[http.MethodDelete] = func() { deleteBook(router.lib, w, r) }
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							p.ServeHTTP(w, r)
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		static(router.static).ServeHTTP(w, r)
 | 
							static(router.static).ServeHTTP(w, r)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,24 +55,26 @@ func main() {
 | 
				
			|||||||
		c.DiscogsToken, time.Hour*24, c.DiscogsUser, c.DiscogsPersist, c.DiscogsCacheFile,
 | 
							c.DiscogsToken, time.Hour*24, c.DiscogsUser, c.DiscogsPersist, c.DiscogsCacheFile,
 | 
				
			||||||
	))
 | 
						))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	frontendRoot := must.Get(frontend.Root())
 | 
						staticRoot := must.Get(frontend.Root())
 | 
				
			||||||
	adminRoot := must.Get(frontend.AdminRoot())
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	servers := make(chan (*http.Server), 3)
 | 
						servers := make(chan (*http.Server), 3)
 | 
				
			||||||
	errGroup := errgroup.Group{}
 | 
						errGroup := errgroup.Group{}
 | 
				
			||||||
	errGroup.Go(func() error {
 | 
						errGroup.Go(func() error {
 | 
				
			||||||
		return start(servers)(
 | 
							return start(servers)(
 | 
				
			||||||
			publicServer(8080, &Router{
 | 
								publicServer(8080, &Router{
 | 
				
			||||||
				static: frontendRoot,
 | 
									static:  staticRoot,
 | 
				
			||||||
				lib:     lib,
 | 
									lib:     lib,
 | 
				
			||||||
				rcol:    discogsCache,
 | 
									rcol:    discogsCache,
 | 
				
			||||||
 | 
									isAdmin: false,
 | 
				
			||||||
			}))
 | 
								}))
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	errGroup.Go(func() error {
 | 
						errGroup.Go(func() error {
 | 
				
			||||||
		return start(servers)(
 | 
							return start(servers)(
 | 
				
			||||||
			tailscaleListener("library-admin", &AdminRouter{
 | 
								tailscaleListener("library-admin", &Router{
 | 
				
			||||||
				static: adminRoot,
 | 
									static:  staticRoot,
 | 
				
			||||||
				lib:     lib,
 | 
									lib:     lib,
 | 
				
			||||||
 | 
									rcol:    discogsCache,
 | 
				
			||||||
 | 
									isAdmin: true,
 | 
				
			||||||
			}))
 | 
								}))
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	errGroup.Go(func() error {
 | 
						errGroup.Go(func() error {
 | 
				
			||||||
@@ -143,10 +145,15 @@ func publicServer(port int, handler http.Handler) (*http.Server, net.Listener, e
 | 
				
			|||||||
	return server, ln, nil
 | 
						return server, ln, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func tailscaleListener(hostname string, handler *AdminRouter) (*http.Server, net.Listener, error) {
 | 
					func tailscaleListener(hostname string, handler *Router) (*http.Server, net.Listener, error) {
 | 
				
			||||||
	s := &tsnet.Server{
 | 
						s := &tsnet.Server{
 | 
				
			||||||
		Dir:      ".config/" + hostname,
 | 
							Dir:      ".config/" + hostname,
 | 
				
			||||||
		Hostname: hostname,
 | 
							Hostname: hostname,
 | 
				
			||||||
 | 
							Logf: func(s string, a ...any) { // silence most tsnet logs
 | 
				
			||||||
 | 
								if strings.HasPrefix(s, "To start this tsnet server") {
 | 
				
			||||||
 | 
									log.Printf(s, a...)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	ln, err := s.Listen("tcp", ":80")
 | 
						ln, err := s.Listen("tcp", ":80")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,217 +0,0 @@
 | 
				
			|||||||
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 `<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>
 | 
					 | 
				
			||||||
  </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="addBook">add book form goes here</div>`;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 4.3 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 69 KiB  | 
@@ -1,47 +0,0 @@
 | 
				
			|||||||
<!DOCTYPE html>
 | 
					 | 
				
			||||||
<html lang="en">
 | 
					 | 
				
			||||||
  <head>
 | 
					 | 
				
			||||||
    <title>Library</title>
 | 
					 | 
				
			||||||
    <link rel="stylesheet" href="style.css" />
 | 
					 | 
				
			||||||
    <link rel="icon" href="favicon.ico" type="image/x-icon" />
 | 
					 | 
				
			||||||
    <link
 | 
					 | 
				
			||||||
      href="https://fonts.googleapis.com/css?family=Libre+Baskerville:400,700&display=swap"
 | 
					 | 
				
			||||||
      as="style"
 | 
					 | 
				
			||||||
      rel="stylesheet preload prefetch"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
    <script type="text/javascript" src="app.js"></script>
 | 
					 | 
				
			||||||
    <script type="text/javascript">
 | 
					 | 
				
			||||||
      window.addEventListener("DOMContentLoaded", init);
 | 
					 | 
				
			||||||
    </script>
 | 
					 | 
				
			||||||
    <meta name="description" content="A personal library record." />
 | 
					 | 
				
			||||||
  </head>
 | 
					 | 
				
			||||||
  <body>
 | 
					 | 
				
			||||||
    <div class="wrapper">
 | 
					 | 
				
			||||||
      <div id="header">
 | 
					 | 
				
			||||||
        <h1>Library</h1>
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noreferrer"
 | 
					 | 
				
			||||||
          href="https://git.yetaga.in/alazyreader/library"
 | 
					 | 
				
			||||||
          >git</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
        <a href="#">add book</a>
 | 
					 | 
				
			||||||
        <div id="searchBox">
 | 
					 | 
				
			||||||
          <label for="childrens" class="bookCount"
 | 
					 | 
				
			||||||
            >Include Childrens Books?</label
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
          <input id="childrens" type="checkbox" name="childrens" />
 | 
					 | 
				
			||||||
          <span id="bookCount" class="bookCount">_ books</span>
 | 
					 | 
				
			||||||
          <input
 | 
					 | 
				
			||||||
            id="search"
 | 
					 | 
				
			||||||
            type="text"
 | 
					 | 
				
			||||||
            name="search"
 | 
					 | 
				
			||||||
            placeholder="Search..."
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div id="current">No Book Selected</div>
 | 
					 | 
				
			||||||
      <div id="books"></div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </body>
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
@@ -1,284 +0,0 @@
 | 
				
			|||||||
/* http://meyerweb.com/eric/tools/css/reset/ 
 | 
					 | 
				
			||||||
   v2.0 | 20110126
 | 
					 | 
				
			||||||
   License: none (public domain)
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
html,
 | 
					 | 
				
			||||||
body,
 | 
					 | 
				
			||||||
div,
 | 
					 | 
				
			||||||
span,
 | 
					 | 
				
			||||||
applet,
 | 
					 | 
				
			||||||
object,
 | 
					 | 
				
			||||||
iframe,
 | 
					 | 
				
			||||||
h1,
 | 
					 | 
				
			||||||
h2,
 | 
					 | 
				
			||||||
h3,
 | 
					 | 
				
			||||||
h4,
 | 
					 | 
				
			||||||
h5,
 | 
					 | 
				
			||||||
h6,
 | 
					 | 
				
			||||||
p,
 | 
					 | 
				
			||||||
blockquote,
 | 
					 | 
				
			||||||
pre,
 | 
					 | 
				
			||||||
a,
 | 
					 | 
				
			||||||
abbr,
 | 
					 | 
				
			||||||
acronym,
 | 
					 | 
				
			||||||
address,
 | 
					 | 
				
			||||||
big,
 | 
					 | 
				
			||||||
cite,
 | 
					 | 
				
			||||||
code,
 | 
					 | 
				
			||||||
del,
 | 
					 | 
				
			||||||
dfn,
 | 
					 | 
				
			||||||
em,
 | 
					 | 
				
			||||||
img,
 | 
					 | 
				
			||||||
ins,
 | 
					 | 
				
			||||||
kbd,
 | 
					 | 
				
			||||||
q,
 | 
					 | 
				
			||||||
s,
 | 
					 | 
				
			||||||
samp,
 | 
					 | 
				
			||||||
small,
 | 
					 | 
				
			||||||
strike,
 | 
					 | 
				
			||||||
strong,
 | 
					 | 
				
			||||||
sub,
 | 
					 | 
				
			||||||
sup,
 | 
					 | 
				
			||||||
tt,
 | 
					 | 
				
			||||||
var,
 | 
					 | 
				
			||||||
b,
 | 
					 | 
				
			||||||
u,
 | 
					 | 
				
			||||||
i,
 | 
					 | 
				
			||||||
center,
 | 
					 | 
				
			||||||
dl,
 | 
					 | 
				
			||||||
dt,
 | 
					 | 
				
			||||||
dd,
 | 
					 | 
				
			||||||
ol,
 | 
					 | 
				
			||||||
ul,
 | 
					 | 
				
			||||||
li,
 | 
					 | 
				
			||||||
fieldset,
 | 
					 | 
				
			||||||
form,
 | 
					 | 
				
			||||||
label,
 | 
					 | 
				
			||||||
legend,
 | 
					 | 
				
			||||||
table,
 | 
					 | 
				
			||||||
caption,
 | 
					 | 
				
			||||||
tbody,
 | 
					 | 
				
			||||||
tfoot,
 | 
					 | 
				
			||||||
thead,
 | 
					 | 
				
			||||||
tr,
 | 
					 | 
				
			||||||
th,
 | 
					 | 
				
			||||||
td,
 | 
					 | 
				
			||||||
article,
 | 
					 | 
				
			||||||
aside,
 | 
					 | 
				
			||||||
canvas,
 | 
					 | 
				
			||||||
details,
 | 
					 | 
				
			||||||
embed,
 | 
					 | 
				
			||||||
figure,
 | 
					 | 
				
			||||||
figcaption,
 | 
					 | 
				
			||||||
footer,
 | 
					 | 
				
			||||||
header,
 | 
					 | 
				
			||||||
hgroup,
 | 
					 | 
				
			||||||
menu,
 | 
					 | 
				
			||||||
nav,
 | 
					 | 
				
			||||||
output,
 | 
					 | 
				
			||||||
ruby,
 | 
					 | 
				
			||||||
section,
 | 
					 | 
				
			||||||
summary,
 | 
					 | 
				
			||||||
time,
 | 
					 | 
				
			||||||
mark,
 | 
					 | 
				
			||||||
audio,
 | 
					 | 
				
			||||||
video {
 | 
					 | 
				
			||||||
  margin: 0;
 | 
					 | 
				
			||||||
  padding: 0;
 | 
					 | 
				
			||||||
  border: 0;
 | 
					 | 
				
			||||||
  font-size: 100%;
 | 
					 | 
				
			||||||
  font: inherit;
 | 
					 | 
				
			||||||
  vertical-align: baseline;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
article,
 | 
					 | 
				
			||||||
aside,
 | 
					 | 
				
			||||||
details,
 | 
					 | 
				
			||||||
figcaption,
 | 
					 | 
				
			||||||
figure,
 | 
					 | 
				
			||||||
footer,
 | 
					 | 
				
			||||||
header,
 | 
					 | 
				
			||||||
hgroup,
 | 
					 | 
				
			||||||
menu,
 | 
					 | 
				
			||||||
nav,
 | 
					 | 
				
			||||||
section {
 | 
					 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
body {
 | 
					 | 
				
			||||||
  line-height: 1;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
ol,
 | 
					 | 
				
			||||||
ul {
 | 
					 | 
				
			||||||
  list-style: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
blockquote,
 | 
					 | 
				
			||||||
q {
 | 
					 | 
				
			||||||
  quotes: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
blockquote:before,
 | 
					 | 
				
			||||||
blockquote:after,
 | 
					 | 
				
			||||||
q:before,
 | 
					 | 
				
			||||||
q:after {
 | 
					 | 
				
			||||||
  content: "";
 | 
					 | 
				
			||||||
  content: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
table {
 | 
					 | 
				
			||||||
  border-collapse: collapse;
 | 
					 | 
				
			||||||
  border-spacing: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* site CSS starts here */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
body {
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#header {
 | 
					 | 
				
			||||||
  height: 30px;
 | 
					 | 
				
			||||||
  width: calc(100vw - 20px);
 | 
					 | 
				
			||||||
  padding: 4px 10px;
 | 
					 | 
				
			||||||
  background-color: #f7f3dc;
 | 
					 | 
				
			||||||
  border-bottom: 2px solid #d8d0a0;
 | 
					 | 
				
			||||||
  font-family: "Libre Baskerville", sans-serif;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#header h1 {
 | 
					 | 
				
			||||||
  font-size: xx-large;
 | 
					 | 
				
			||||||
  display: inline;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#header .bookCount {
 | 
					 | 
				
			||||||
  font-size: small;
 | 
					 | 
				
			||||||
  color: #a29c77;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#searchBox {
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  right: 10px;
 | 
					 | 
				
			||||||
  top: 7px;
 | 
					 | 
				
			||||||
  text-align: right;
 | 
					 | 
				
			||||||
  width: 800px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#searchBox input#search {
 | 
					 | 
				
			||||||
  width: 300px;
 | 
					 | 
				
			||||||
  font-size: 16px;
 | 
					 | 
				
			||||||
  background: #f9f8ed;
 | 
					 | 
				
			||||||
  padding: 2px 5px;
 | 
					 | 
				
			||||||
  border: none;
 | 
					 | 
				
			||||||
  border-bottom: 2px solid #d8d0a0;
 | 
					 | 
				
			||||||
  font-family: "Libre Baskerville", sans-serif;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#searchBox input:focus {
 | 
					 | 
				
			||||||
  outline: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#searchBox input::placeholder {
 | 
					 | 
				
			||||||
  font-family: "Libre Baskerville", sans-serif;
 | 
					 | 
				
			||||||
  color: #d8d0a0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#current {
 | 
					 | 
				
			||||||
  background-color: #f7f3dc;
 | 
					 | 
				
			||||||
  width: calc(40vw - 40px);
 | 
					 | 
				
			||||||
  height: calc(100vh - 80px);
 | 
					 | 
				
			||||||
  padding: 20px;
 | 
					 | 
				
			||||||
  overflow: auto;
 | 
					 | 
				
			||||||
  float: left;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#books {
 | 
					 | 
				
			||||||
  width: calc(60vw - 40px);
 | 
					 | 
				
			||||||
  height: calc(100vh - 80px);
 | 
					 | 
				
			||||||
  padding: 20px;
 | 
					 | 
				
			||||||
  overflow: auto;
 | 
					 | 
				
			||||||
  float: left;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.bookTable th {
 | 
					 | 
				
			||||||
  font-weight: bold;
 | 
					 | 
				
			||||||
  text-align: left;
 | 
					 | 
				
			||||||
  font-family: "Libre Baskerville", sans-serif;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.bookTable th[data-sort-by] {
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.bookTable th[data-sort-by]::after {
 | 
					 | 
				
			||||||
  content: "\2195";
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  left: 4px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.bookTable th.asc::after {
 | 
					 | 
				
			||||||
  content: "\2191";
 | 
					 | 
				
			||||||
  font-size: small;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  left: 4px;
 | 
					 | 
				
			||||||
  bottom: 1px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.bookTable th.desc::after {
 | 
					 | 
				
			||||||
  content: "\2193";
 | 
					 | 
				
			||||||
  font-size: small;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  left: 4px;
 | 
					 | 
				
			||||||
  bottom: 1px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.bookTable td,
 | 
					 | 
				
			||||||
.bookTable th {
 | 
					 | 
				
			||||||
  padding: 5px;
 | 
					 | 
				
			||||||
  min-width: 50px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.tRow:nth-child(odd) {
 | 
					 | 
				
			||||||
  background: #f9f8ed;
 | 
					 | 
				
			||||||
  border-bottom: 1px solid #d8d0a0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.bookTable .tRow {
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.bookTable .tRow .title {
 | 
					 | 
				
			||||||
  font-style: italic;
 | 
					 | 
				
			||||||
  max-width: 600px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#current h1 {
 | 
					 | 
				
			||||||
  font-size: x-large;
 | 
					 | 
				
			||||||
  font-weight: bold;
 | 
					 | 
				
			||||||
  font-style: italic;
 | 
					 | 
				
			||||||
  padding: 0 0 5px 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#current h2 {
 | 
					 | 
				
			||||||
  font-size: large;
 | 
					 | 
				
			||||||
  padding: 7px 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#current img {
 | 
					 | 
				
			||||||
  opacity: 0.5;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  left: 0;
 | 
					 | 
				
			||||||
  top: 0;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  height: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#current .bookDetails {
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  background-color: rgba(255, 255, 255, 0.8);
 | 
					 | 
				
			||||||
  padding: 10px;
 | 
					 | 
				
			||||||
  margin: 0;
 | 
					 | 
				
			||||||
  width: 75%;
 | 
					 | 
				
			||||||
  border-radius: 5px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#current .description p {
 | 
					 | 
				
			||||||
  padding: 20px 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -8,13 +8,6 @@ import (
 | 
				
			|||||||
//go:embed files
 | 
					//go:embed files
 | 
				
			||||||
var static embed.FS
 | 
					var static embed.FS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//go:embed admin
 | 
					 | 
				
			||||||
var admin embed.FS
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func Root() (fs.FS, error) {
 | 
					func Root() (fs.FS, error) {
 | 
				
			||||||
	return fs.Sub(static, "files")
 | 
						return fs.Sub(static, "files")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func AdminRoot() (fs.FS, error) {
 | 
					 | 
				
			||||||
	return fs.Sub(admin, "admin")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user