From 7459117b12a54b7df369e807b9897e1ee0a3bf3a Mon Sep 17 00:00:00 2001 From: David Ashby Date: Wed, 3 Jan 2024 22:11:01 -0500 Subject: [PATCH] finish the frontend functionality; needs styling --- cmd/serve/api.go | 72 +++++++++++++-------------- cmd/serve/main.go | 41 +++++++--------- frontend/files/app.js | 112 ++++++++++++++++++++++++++++++++---------- go.mod | 2 +- query/googlebooks.go | 2 +- 5 files changed, 139 insertions(+), 90 deletions(-) diff --git a/cmd/serve/api.go b/cmd/serve/api.go index df8ce9f..e2c892a 100644 --- a/cmd/serve/api.go +++ b/cmd/serve/api.go @@ -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 +} diff --git a/cmd/serve/main.go b/cmd/serve/main.go index dd20ff1..adb6471 100644 --- a/cmd/serve/main.go +++ b/cmd/serve/main.go @@ -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 } diff --git a/frontend/files/app.js b/frontend/files/app.js index 8e2a59b..e3b4431 100644 --- a/frontend/files/app.js +++ b/frontend/files/app.js @@ -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 `
-
${[ - { 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( - `
` - ); - }, "")} -
+
+ ${[ + { 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( + `
` + ); + }, "")} + + +
`; } diff --git a/go.mod b/go.mod index f5e0b56..fffbf1c 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/query/googlebooks.go b/query/googlebooks.go index 7479b7f..4825eca 100644 --- a/query/googlebooks.go +++ b/query/googlebooks.go @@ -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"`