finish the frontend functionality; needs styling
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
b6119f320f
commit
7459117b12
@ -2,8 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.yetaga.in/alazyreader/library/media"
|
"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) {
|
func writeJSONerror(w http.ResponseWriter, err string, status int) {
|
||||||
|
log.Println(err)
|
||||||
writeJSON(w, struct{ Status, Reason string }{Status: "error", Reason: err}, status)
|
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) {
|
func addBook(l Library, w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Body == nil {
|
book, err := ReadBody[media.Book](r.Body)
|
||||||
writeJSONerror(w, "no body provided", http.StatusBadRequest)
|
if err != nil {
|
||||||
|
writeJSONerror(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer r.Body.Close()
|
if err = l.AddBook(r.Context(), book); err != nil {
|
||||||
b, err := io.ReadAll(r.Body)
|
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusAccepted)
|
w.WriteHeader(http.StatusAccepted)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteBook(l Library, w http.ResponseWriter, r *http.Request) {
|
func deleteBook(l Library, w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Body == nil {
|
book, err := ReadBody[media.Book](r.Body)
|
||||||
writeJSONerror(w, "no body provided", http.StatusBadRequest)
|
if err != nil {
|
||||||
|
writeJSONerror(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer r.Body.Close()
|
if err = l.DeleteBook(r.Context(), book); err != nil {
|
||||||
b, err := io.ReadAll(r.Body)
|
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusAccepted)
|
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) {
|
func lookupBook(query Query, w http.ResponseWriter, r *http.Request) {
|
||||||
isbn := r.FormValue("isbn")
|
req, err := ReadBody[media.Book](r.Body)
|
||||||
if len(isbn) != 10 && len(isbn) != 13 {
|
if err != nil {
|
||||||
writeJSONerror(w, "invalid isbn", http.StatusBadRequest)
|
writeJSONerror(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
book, err := query.GetByISBN(isbn)
|
book, err := query.GetByISBN(req.ISBN13)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
|
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -181,3 +160,20 @@ func lookupBook(query Query, w http.ResponseWriter, r *http.Request) {
|
|||||||
func static(f fs.FS) http.Handler {
|
func static(f fs.FS) http.Handler {
|
||||||
return http.FileServer(http.FS(f))
|
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
|
||||||
|
}
|
||||||
|
@ -45,50 +45,44 @@ func main() {
|
|||||||
must.Do(envconfig.Process("library", &c))
|
must.Do(envconfig.Process("library", &c))
|
||||||
|
|
||||||
var lib Library
|
var lib Library
|
||||||
var err error
|
|
||||||
if c.DBType == "memory" {
|
if c.DBType == "memory" {
|
||||||
lib = &database.Memory{}
|
lib = &database.Memory{}
|
||||||
} else if c.DBType == "sql" {
|
} else if c.DBType == "sql" {
|
||||||
var latest, run int
|
sqllib, latest, run, err := setupSQL(c)
|
||||||
lib, latest, run, err = setupSQL(c)
|
|
||||||
if err != nil {
|
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)
|
log.Printf("latest migration: %d; migrations run: %d", latest, run)
|
||||||
|
lib = sqllib
|
||||||
}
|
}
|
||||||
discogsCache := must.Get(database.NewDiscogsCache(
|
discogsCache := must.Get(database.NewDiscogsCache(
|
||||||
c.DiscogsToken, time.Hour*24, c.DiscogsUser, c.DiscogsPersist, c.DiscogsCacheFile,
|
c.DiscogsToken, time.Hour*24, c.DiscogsUser, c.DiscogsPersist, c.DiscogsCacheFile,
|
||||||
))
|
))
|
||||||
|
|
||||||
queryProvider := &query.GoogleBooks{}
|
queryProvider := &query.GoogleBooks{}
|
||||||
|
|
||||||
staticRoot := must.Get(frontend.Root())
|
staticRoot := must.Get(frontend.Root())
|
||||||
|
|
||||||
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: staticRoot,
|
||||||
static: staticRoot,
|
lib: lib,
|
||||||
lib: lib,
|
rcol: discogsCache,
|
||||||
rcol: discogsCache,
|
isAdmin: false,
|
||||||
isAdmin: false,
|
}))
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
errGroup.Go(func() error {
|
errGroup.Go(func() error {
|
||||||
return start(servers)(
|
return start(servers)(tailscaleListener("library-admin", &Router{
|
||||||
tailscaleListener("library-admin", &Router{
|
static: staticRoot,
|
||||||
static: staticRoot,
|
lib: lib,
|
||||||
lib: lib,
|
rcol: discogsCache,
|
||||||
rcol: discogsCache,
|
query: queryProvider,
|
||||||
query: queryProvider,
|
isAdmin: true,
|
||||||
isAdmin: true,
|
}))
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
errGroup.Go(func() error {
|
errGroup.Go(func() error {
|
||||||
return shutdown(servers)
|
return shutdown(servers)
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Println(errGroup.Wait())
|
log.Println(errGroup.Wait())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +166,5 @@ func tailscaleListener(hostname string, handler *Router) (*http.Server, net.List
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
log.Printf("management server: http://%s/", hostname)
|
log.Printf("management server: http://%s/", hostname)
|
||||||
server := &http.Server{Handler: handler}
|
return &http.Server{Handler: handler}, ln, nil
|
||||||
return server, ln, nil
|
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,62 @@ function init() {
|
|||||||
|
|
||||||
function renderAddBookView() {
|
function renderAddBookView() {
|
||||||
document.getElementById("current").innerHTML = AddBookTemplate();
|
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) {
|
function renderTable(books, sortField) {
|
||||||
@ -94,7 +150,8 @@ function renderTable(books, sortField) {
|
|||||||
Array.from(bookElement.querySelectorAll("tbody tr th[data-sort-by]")).forEach(
|
Array.from(bookElement.querySelectorAll("tbody tr th[data-sort-by]")).forEach(
|
||||||
(row) => {
|
(row) => {
|
||||||
row.addEventListener("click", function (e) {
|
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,
|
"isbn-10": isbn10,
|
||||||
authors,
|
authors,
|
||||||
coverURL,
|
coverURL,
|
||||||
description,
|
|
||||||
format,
|
format,
|
||||||
notes,
|
|
||||||
publisher,
|
publisher,
|
||||||
series,
|
series,
|
||||||
signed,
|
signed,
|
||||||
@ -235,28 +290,33 @@ function TableTemplate(books) {
|
|||||||
|
|
||||||
function AddBookTemplate() {
|
function AddBookTemplate() {
|
||||||
return `<div class="addBookView">
|
return `<div class="addBookView">
|
||||||
<form>${[
|
<div id="newBookForm">
|
||||||
{ name: "ISBN10", type: "text" },
|
${[
|
||||||
{ name: "ISBN13", type: "text" },
|
{ name: "Title", id: "title", type: "text" },
|
||||||
{ name: "Title", type: "text" },
|
{ name: "Authors", id: "authors", type: "text" },
|
||||||
{ name: "Authors", type: "text" },
|
{ name: "SortAuthor", id: "sortAuthor", type: "text" },
|
||||||
{ name: "SortAuthor", type: "text" },
|
{ name: "ISBN10", id: "isbn-10", type: "text" },
|
||||||
{ name: "Format", type: "text" },
|
{ name: "ISBN13", id: "isbn-13", type: "text" },
|
||||||
{ name: "Genre", type: "text" },
|
{ name: "Publisher", id: "publisher", type: "text" },
|
||||||
{ name: "Publisher", type: "text" },
|
{ name: "Format", id: "format", type: "text" },
|
||||||
{ name: "Series", type: "text" },
|
{ name: "Genre", id: "genre", type: "text" },
|
||||||
{ name: "Volume", type: "text" },
|
{ name: "Series", id: "series", type: "text" },
|
||||||
{ name: "Year", type: "text" },
|
{ name: "Volume", id: "volume", type: "text" },
|
||||||
{ name: "Signed", type: "checkbox" },
|
{ name: "Year", id: "year", type: "text" },
|
||||||
// { name: "Description", type: "text" },
|
{ name: "CoverURL", id: "coverURL", type: "text" },
|
||||||
// { name: "Notes", type: "text" },
|
{ name: "Signed", id: "signed", type: "checkbox" },
|
||||||
{ name: "CoverURL", type: "text" },
|
{ name: "Childrens", id: "childrens", type: "checkbox" },
|
||||||
{ name: "Childrens", type: "checkbox" },
|
].reduce((acc, field) => {
|
||||||
].reduce((acc, field) => {
|
return acc.concat(
|
||||||
return acc.concat(
|
`<label>${field.name} <input
|
||||||
`<label>${field.name} <input type="${field.type}" name="${field.name.toLowerCase}"/></label><br/>`
|
type="${field.type}"
|
||||||
);
|
name="${field.name.toLowerCase()}"
|
||||||
}, "")}
|
id="${field.id}"
|
||||||
</form>
|
/></label><br/>`
|
||||||
|
);
|
||||||
|
}, "")}
|
||||||
|
<input id="lookup" type="submit" value="look up">
|
||||||
|
<input id="save" type="submit" value="save">
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -5,6 +5,7 @@ go 1.21
|
|||||||
toolchain go1.21.5
|
toolchain go1.21.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.yetaga.in/alazyreader/go-openlibrary v0.0.1
|
||||||
github.com/gdamore/tcell/v2 v2.7.0
|
github.com/gdamore/tcell/v2 v2.7.0
|
||||||
github.com/go-sql-driver/mysql v1.7.1
|
github.com/go-sql-driver/mysql v1.7.1
|
||||||
github.com/irlndts/go-discogs v0.3.6
|
github.com/irlndts/go-discogs v0.3.6
|
||||||
@ -15,7 +16,6 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.0.0 // indirect
|
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/akutz/memconn v0.1.0 // indirect
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.21.0 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.21.0 // indirect
|
||||||
|
@ -50,7 +50,7 @@ type volumeInfo struct {
|
|||||||
PageCount int `json:"pageCount"`
|
PageCount int `json:"pageCount"`
|
||||||
PrintType string `json:"printType"`
|
PrintType string `json:"printType"`
|
||||||
Categories []string `json:"categories"`
|
Categories []string `json:"categories"`
|
||||||
AverageRating int `json:"averageRating"`
|
AverageRating float64 `json:"averageRating"`
|
||||||
RatingsCount int `json:"ratingsCount"`
|
RatingsCount int `json:"ratingsCount"`
|
||||||
MaturityRating string `json:"maturityRating"`
|
MaturityRating string `json:"maturityRating"`
|
||||||
AllowAnonLogging bool `json:"allowAnonLogging"`
|
AllowAnonLogging bool `json:"allowAnonLogging"`
|
||||||
|
Loading…
Reference in New Issue
Block a user