finish the frontend functionality; needs styling
ci/woodpecker/push/woodpecker Pipeline was successful Details

This commit is contained in:
David 2024-01-03 22:11:01 -05:00
parent b6119f320f
commit 7459117b12
5 changed files with 139 additions and 90 deletions

View File

@ -2,8 +2,10 @@ package main
import (
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"git.yetaga.in/alazyreader/library/media"
@ -30,6 +32,7 @@ func (h path) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func writeJSONerror(w http.ResponseWriter, err string, status int) {
log.Println(err)
writeJSON(w, struct{ Status, Reason string }{Status: "error", Reason: err}, status)
}
@ -97,50 +100,26 @@ func getBooks(l Library, w http.ResponseWriter, r *http.Request) {
}
func addBook(l Library, w http.ResponseWriter, r *http.Request) {
if r.Body == nil {
writeJSONerror(w, "no body provided", http.StatusBadRequest)
book, err := ReadBody[media.Book](r.Body)
if err != nil {
writeJSONerror(w, err.Error(), http.StatusBadRequest)
return
}
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
if err != nil {
writeJSONerror(w, "error reading body", http.StatusBadRequest)
return
}
book := &media.Book{}
err = json.Unmarshal(b, book)
if err != nil {
writeJSONerror(w, "error parsing body", http.StatusBadRequest)
return
}
err = l.AddBook(r.Context(), book)
if err != nil {
writeJSONerror(w, "error parsing body", http.StatusBadRequest)
if err = l.AddBook(r.Context(), book); err != nil {
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusAccepted)
}
func deleteBook(l Library, w http.ResponseWriter, r *http.Request) {
if r.Body == nil {
writeJSONerror(w, "no body provided", http.StatusBadRequest)
book, err := ReadBody[media.Book](r.Body)
if err != nil {
writeJSONerror(w, err.Error(), http.StatusBadRequest)
return
}
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
if err != nil {
writeJSONerror(w, "error reading body", http.StatusBadRequest)
return
}
book := &media.Book{}
err = json.Unmarshal(b, book)
if err != nil {
writeJSONerror(w, "error parsing body", http.StatusBadRequest)
return
}
err = l.DeleteBook(r.Context(), book)
if err != nil {
writeJSONerror(w, "error deleting book", http.StatusInternalServerError)
if err = l.DeleteBook(r.Context(), book); err != nil {
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusAccepted)
@ -165,12 +144,12 @@ func getWhoAmI(ts *tailscale.LocalClient, w http.ResponseWriter, r *http.Request
}
func lookupBook(query Query, w http.ResponseWriter, r *http.Request) {
isbn := r.FormValue("isbn")
if len(isbn) != 10 && len(isbn) != 13 {
writeJSONerror(w, "invalid isbn", http.StatusBadRequest)
req, err := ReadBody[media.Book](r.Body)
if err != nil {
writeJSONerror(w, err.Error(), http.StatusBadRequest)
return
}
book, err := query.GetByISBN(isbn)
book, err := query.GetByISBN(req.ISBN13)
if err != nil {
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
return
@ -181,3 +160,20 @@ func lookupBook(query Query, w http.ResponseWriter, r *http.Request) {
func static(f fs.FS) http.Handler {
return http.FileServer(http.FS(f))
}
func ReadBody[T any](r io.ReadCloser) (*T, error) {
t := new(T)
if r == nil {
return t, fmt.Errorf("no body provided")
}
defer r.Close()
b, err := io.ReadAll(r)
if err != nil {
return t, fmt.Errorf("error reading body: %w", err)
}
err = json.Unmarshal(b, t)
if err != nil {
return t, fmt.Errorf("error reading body: %w", err)
}
return t, nil
}

View File

@ -45,50 +45,44 @@ func main() {
must.Do(envconfig.Process("library", &c))
var lib Library
var err error
if c.DBType == "memory" {
lib = &database.Memory{}
} else if c.DBType == "sql" {
var latest, run int
lib, latest, run, err = setupSQL(c)
sqllib, latest, run, err := setupSQL(c)
if err != nil {
log.Fatalf("err starting sql connection: %v", err)
log.Fatalf("sql connection err: %v", err)
}
log.Printf("latest migration: %d; migrations run: %d", latest, run)
lib = sqllib
}
discogsCache := must.Get(database.NewDiscogsCache(
c.DiscogsToken, time.Hour*24, c.DiscogsUser, c.DiscogsPersist, c.DiscogsCacheFile,
))
queryProvider := &query.GoogleBooks{}
staticRoot := must.Get(frontend.Root())
servers := make(chan (*http.Server), 3)
errGroup := errgroup.Group{}
errGroup.Go(func() error {
return start(servers)(
publicServer(8080, &Router{
static: staticRoot,
lib: lib,
rcol: discogsCache,
isAdmin: false,
}))
return start(servers)(publicServer(8080, &Router{
static: staticRoot,
lib: lib,
rcol: discogsCache,
isAdmin: false,
}))
})
errGroup.Go(func() error {
return start(servers)(
tailscaleListener("library-admin", &Router{
static: staticRoot,
lib: lib,
rcol: discogsCache,
query: queryProvider,
isAdmin: true,
}))
return start(servers)(tailscaleListener("library-admin", &Router{
static: staticRoot,
lib: lib,
rcol: discogsCache,
query: queryProvider,
isAdmin: true,
}))
})
errGroup.Go(func() error {
return shutdown(servers)
})
log.Println(errGroup.Wait())
}
@ -172,6 +166,5 @@ func tailscaleListener(hostname string, handler *Router) (*http.Server, net.List
return nil, nil, err
}
log.Printf("management server: http://%s/", hostname)
server := &http.Server{Handler: handler}
return server, ln, nil
return &http.Server{Handler: handler}, ln, nil
}

View File

@ -51,6 +51,62 @@ function init() {
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();
fetch("/api/books", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
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,
}),
});
renderAddBookView();
init();
});
}
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),
});
}
function renderTable(books, sortField) {
@ -94,7 +150,8 @@ function renderTable(books, sortField) {
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
// only add callback when there's a sortBy attribute
renderTable(books, e.target.dataset.sortBy);
});
}
);
@ -170,9 +227,7 @@ function BookTemplate({
"isbn-10": isbn10,
authors,
coverURL,
description,
format,
notes,
publisher,
series,
signed,
@ -235,28 +290,33 @@ function TableTemplate(books) {
function AddBookTemplate() {
return `<div class="addBookView">
<form>${[
{ name: "ISBN10", type: "text" },
{ name: "ISBN13", type: "text" },
{ name: "Title", type: "text" },
{ name: "Authors", type: "text" },
{ name: "SortAuthor", type: "text" },
{ name: "Format", type: "text" },
{ name: "Genre", type: "text" },
{ name: "Publisher", type: "text" },
{ name: "Series", type: "text" },
{ name: "Volume", type: "text" },
{ name: "Year", type: "text" },
{ name: "Signed", type: "checkbox" },
// { name: "Description", type: "text" },
// { name: "Notes", type: "text" },
{ name: "CoverURL", type: "text" },
{ name: "Childrens", type: "checkbox" },
].reduce((acc, field) => {
return acc.concat(
`<label>${field.name} <input type="${field.type}" name="${field.name.toLowerCase}"/></label><br/>`
);
}, "")}
</form>
<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>`;
}

2
go.mod
View File

@ -5,6 +5,7 @@ go 1.21
toolchain go1.21.5
require (
git.yetaga.in/alazyreader/go-openlibrary v0.0.1
github.com/gdamore/tcell/v2 v2.7.0
github.com/go-sql-driver/mysql v1.7.1
github.com/irlndts/go-discogs v0.3.6
@ -15,7 +16,6 @@ require (
require (
filippo.io/edwards25519 v1.0.0 // indirect
git.yetaga.in/alazyreader/go-openlibrary v0.0.1 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/aws/aws-sdk-go-v2 v1.21.0 // indirect

View File

@ -50,7 +50,7 @@ type volumeInfo struct {
PageCount int `json:"pageCount"`
PrintType string `json:"printType"`
Categories []string `json:"categories"`
AverageRating int `json:"averageRating"`
AverageRating float64 `json:"averageRating"`
RatingsCount int `json:"ratingsCount"`
MaturityRating string `json:"maturityRating"`
AllowAnonLogging bool `json:"allowAnonLogging"`