[add] docker instructions, [add] metadata gathering, [add] screenshots

This commit is contained in:
Evan Reichard 2023-09-26 18:09:02 -04:00
parent c22154ed77
commit 9ac55a0be0
27 changed files with 773 additions and 537 deletions

View File

@ -1,19 +1,35 @@
FROM alpine:edge AS build # Certificate Store
RUN apk add --no-cache --update go gcc g++ FROM alpine as certs
WORKDIR /app RUN apk update && apk add ca-certificates
COPY . /app
# Copy Resources # Build Image
FROM --platform=$BUILDPLATFORM golang:1.20 AS build
# Install Dependencies
RUN apt-get update -y
RUN apt install -y gcc-x86-64-linux-gnu
# Create Package Directory
WORKDIR /src
RUN mkdir -p /opt/bookmanager RUN mkdir -p /opt/bookmanager
RUN cp -a ./templates /opt/bookmanager/templates
RUN cp -a ./assets /opt/bookmanager/assets
# Download Dependencies & Compile # Cache Dependencies & Compile
RUN go mod download ARG TARGETOS
RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o /opt/bookmanager/server ARG TARGETARCH
RUN --mount=target=. \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
if [ "$TARGETARCH" = "amd64" ]; then \
GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" CC=x86_64-linux-gnu-gcc go build -o /opt/bookmanager/server; \
else \
GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o /opt/bookmanager/server; \
fi; \
cp -a ./templates /opt/bookmanager/templates; \
cp -a ./assets /opt/bookmanager/assets;
# Create Image # Create Image
FROM alpine:3.18 FROM busybox:1.36
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
COPY --from=build /opt/bookmanager /opt/bookmanager COPY --from=build /opt/bookmanager /opt/bookmanager
WORKDIR /opt/bookmanager WORKDIR /opt/bookmanager
EXPOSE 8585 EXPOSE 8585

View File

@ -14,9 +14,13 @@ docker_build_local:
docker_build_release_beta: docker_build_release_beta:
docker buildx build \ docker buildx build \
--platform linux/amd64,linux/arm64 \ --platform linux/amd64,linux/arm64 \
-t gitea.va.reichard.io/reichard/bookmanager:beta --push . -t gitea.va.reichard.io/reichard/bookmanager:beta \
-t gitea.va.reichard.io/evan/bookmanager:beta \
--push .
docker_build_release_latest: docker_build_release_latest:
docker buildx build \ docker buildx build \
--platform linux/amd64,linux/arm64 \ --platform linux/amd64,linux/arm64 \
-t gitea.va.reichard.io/reichard/bookmanager:latest --push . -t gitea.va.reichard.io/reichard/bookmanager:latest \
-t gitea.va.reichard.io/evan/bookmanager:latest \
--push .

View File

@ -1,14 +1,20 @@
# Book Manager # Book Manager
<p align="center"> <p align="center">
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_login.png"> <a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_login.png" width="30%"> <img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png" width="18%">
</a> </a>
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_home.png"> <a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_home.png" width="30%"> <img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png" width="18%">
</a> </a>
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_documents.png"> <a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_documents.png" width="30%"> <img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png" width="18%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/document.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/document.png" width="18%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/metadata.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/metadata.png" width="18%">
</a> </a>
</p> </p>
@ -24,11 +30,30 @@ In additional to the compatible KOSync API's, we add:
- Additional APIs to automatically upload reading statistics - Additional APIs to automatically upload reading statistics
- Automatically upload documents to the server (can download in the "Documents" view) - Automatically upload documents to the server (can download in the "Documents" view)
- Automatic book cover metadata scraping (Thanks [OpenLibrary](https://openlibrary.org/)) - Book metadata scraping (Thanks [OpenLibrary](https://openlibrary.org/) & [Google Books API](https://developers.google.com/books/docs/v1/getting_started))
- No JavaScript! All information is rendered server side. - No JavaScript! All information is rendered server side.
# Server # Server
Docker Image: `docker pull gitea.va.reichard.io/evan/bookmanager:latest`
## Quick Start
```bash
# Make Data Directory
mkdir -p bookmanager_data
# Run Server
docker run \
-p 8585:8585 \
-e REGISTRATION_ENABLED=true \
-v ./bookmanager_data:/config \
-v ./bookmanager_data:/data \
gitea.va.reichard.io/evan/bookmanager:latest
```
The service is now accessible at: `http://localhost:8585`
## Configuration ## Configuration
| Environment Variable | Default Value | Description | | Environment Variable | Default Value | Description |
@ -50,22 +75,22 @@ See documentation in the `client` subfolder: [SyncNinja](https://gitea.va.reicha
SQLC Generation (v1.21.0): SQLC Generation (v1.21.0):
``` ```bash
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
~/go/bin/sqlc generate ~/go/bin/sqlc generate
``` ```
Run Development: Run Development:
``` ```bash
CONFIG_PATH=./data DATA_PATH=./data go run cmd/main.go serve CONFIG_PATH=./data DATA_PATH=./data go run main.go serve
``` ```
# Building # Building
The `Dockerfile` and `Makefile` contain the build information: The `Dockerfile` and `Makefile` contain the build information:
``` ```bash
# Build Local Docker Image # Build Local Docker Image
make docker_build_local make docker_build_local
@ -75,12 +100,12 @@ make docker_build_release_latest
If manually building, you must enable CGO: If manually building, you must enable CGO:
``` ```bash
# Download Dependencies # Download Dependencies
go mod download go mod download
# Compile (Binary `./bookmanager`) # Compile (Binary `./bookmanager`)
CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o /bookmanager cmd/main.go CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o /bookmanager
``` ```
## Notes ## Notes

View File

@ -21,8 +21,11 @@ type requestDocumentEdit struct {
Title *string `form:"title"` Title *string `form:"title"`
Author *string `form:"author"` Author *string `form:"author"`
Description *string `form:"description"` Description *string `form:"description"`
ISBN10 *string `form:"isbn_10"`
ISBN13 *string `form:"isbn_13"`
RemoveCover *string `form:"remove_cover"` RemoveCover *string `form:"remove_cover"`
CoverFile *multipart.FileHeader `form:"cover"` CoverGBID *string `form:"cover_gbid"`
CoverFile *multipart.FileHeader `form:"cover_file"`
} }
type requestDocumentIdentify struct { type requestDocumentIdentify struct {
@ -101,6 +104,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
return return
} }
templateVars["RelBase"] = "../"
templateVars["Data"] = document templateVars["Data"] = document
} else if routeName == "activity" { } else if routeName == "activity" {
activityFilter := database.GetActivityParams{ activityFilter := database.GetActivityParams{
@ -131,7 +135,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
if err != nil { if err != nil {
log.Warn("[createAppResourcesRoute] GetUserWindowStreaks DB Error:", err) log.Warn("[createAppResourcesRoute] GetUserWindowStreaks DB Error:", err)
} }
log.Info("GetUserWindowStreaks - WEEK - ", time.Since(start_time)) log.Debug("GetUserWindowStreaks - WEEK - ", time.Since(start_time))
start_time = time.Now() start_time = time.Now()
daily_streak, err := api.DB.Queries.GetUserWindowStreaks(api.DB.Ctx, database.GetUserWindowStreaksParams{ daily_streak, err := api.DB.Queries.GetUserWindowStreaks(api.DB.Ctx, database.GetUserWindowStreaksParams{
@ -141,15 +145,15 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
if err != nil { if err != nil {
log.Warn("[createAppResourcesRoute] GetUserWindowStreaks DB Error:", err) log.Warn("[createAppResourcesRoute] GetUserWindowStreaks DB Error:", err)
} }
log.Info("GetUserWindowStreaks - DAY - ", time.Since(start_time)) log.Debug("GetUserWindowStreaks - DAY - ", time.Since(start_time))
start_time = time.Now() start_time = time.Now()
database_info, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, rUser.(string)) database_info, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, rUser.(string))
log.Info("GetDatabaseInfo - ", time.Since(start_time)) log.Debug("GetDatabaseInfo - ", time.Since(start_time))
start_time = time.Now() start_time = time.Now()
read_graph_data, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, rUser.(string)) read_graph_data, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, rUser.(string))
log.Info("GetDailyReadStats - ", time.Since(start_time)) log.Debug("GetDailyReadStats - ", time.Since(start_time))
templateVars["Data"] = gin.H{ templateVars["Data"] = gin.H{
"DailyStreak": daily_streak, "DailyStreak": daily_streak,
@ -218,7 +222,7 @@ func (api *API) getDocumentCover(c *gin.Context) {
firstResult := metadataResults[0] firstResult := metadataResults[0]
// Save Cover // Save Cover
fileName, err := metadata.SaveCover(*firstResult.GBID, coverDir, document.ID) fileName, err := metadata.SaveCover(*firstResult.GBID, coverDir, document.ID, false)
if err == nil { if err == nil {
coverFile = *fileName coverFile = *fileName
} }
@ -275,8 +279,11 @@ func (api *API) editDocument(c *gin.Context) {
if rDocEdit.Author == nil && if rDocEdit.Author == nil &&
rDocEdit.Title == nil && rDocEdit.Title == nil &&
rDocEdit.Description == nil && rDocEdit.Description == nil &&
rDocEdit.CoverFile == nil && rDocEdit.ISBN10 == nil &&
rDocEdit.RemoveCover == nil { rDocEdit.ISBN13 == nil &&
rDocEdit.RemoveCover == nil &&
rDocEdit.CoverGBID == nil &&
rDocEdit.CoverFile == nil {
log.Error("[createAppResourcesRoute] Missing Form Values") log.Error("[createAppResourcesRoute] Missing Form Values")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"}) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return return
@ -288,7 +295,6 @@ func (api *API) editDocument(c *gin.Context) {
s := "UNKNOWN" s := "UNKNOWN"
coverFileName = &s coverFileName = &s
} else if rDocEdit.CoverFile != nil { } else if rDocEdit.CoverFile != nil {
// Validate Type & Derive Extension on MIME // Validate Type & Derive Extension on MIME
uploadedFile, err := rDocEdit.CoverFile.Open() uploadedFile, err := rDocEdit.CoverFile.Open()
if err != nil { if err != nil {
@ -325,6 +331,14 @@ func (api *API) editDocument(c *gin.Context) {
} }
coverFileName = &fileName coverFileName = &fileName
} else if rDocEdit.CoverGBID != nil {
// TODO
var coverDir string = filepath.Join(api.Config.DataPath, "covers")
fileName, err := metadata.SaveCover(*rDocEdit.CoverGBID, coverDir, rDocID.DocumentID, true)
if err == nil {
coverFileName = fileName
}
} }
// Update Document // Update Document
@ -333,6 +347,8 @@ func (api *API) editDocument(c *gin.Context) {
Title: api.sanitizeInput(rDocEdit.Title), Title: api.sanitizeInput(rDocEdit.Title),
Author: api.sanitizeInput(rDocEdit.Author), Author: api.sanitizeInput(rDocEdit.Author),
Description: api.sanitizeInput(rDocEdit.Description), Description: api.sanitizeInput(rDocEdit.Description),
Isbn10: api.sanitizeInput(rDocEdit.ISBN10),
Isbn13: api.sanitizeInput(rDocEdit.ISBN13),
Coverfile: coverFileName, Coverfile: coverFileName,
}); err != nil { }); err != nil {
log.Error("[createAppResourcesRoute] UpsertDocument DB Error:", err) log.Error("[createAppResourcesRoute] UpsertDocument DB Error:", err)
@ -367,6 +383,8 @@ func (api *API) deleteDocument(c *gin.Context) {
} }
func (api *API) identifyDocument(c *gin.Context) { func (api *API) identifyDocument(c *gin.Context) {
rUser, _ := c.Get("AuthorizedUser")
var rDocID requestDocumentID var rDocID requestDocumentID
if err := c.ShouldBindUri(&rDocID); err != nil { if err := c.ShouldBindUri(&rDocID); err != nil {
log.Error("[identifyDocument] Invalid URI Bind") log.Error("[identifyDocument] Invalid URI Bind")
@ -399,36 +417,52 @@ func (api *API) identifyDocument(c *gin.Context) {
return return
} }
// Template Variables
templateVars := gin.H{
"RelBase": "../../",
}
// Get Metadata
metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{ metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
Title: rDocIdentify.Title, Title: rDocIdentify.Title,
Author: rDocIdentify.Author, Author: rDocIdentify.Author,
ISBN10: rDocIdentify.ISBN, ISBN10: rDocIdentify.ISBN,
ISBN13: rDocIdentify.ISBN, ISBN13: rDocIdentify.ISBN,
}) })
if err != nil || len(metadataResults) == 0 { if err == nil && len(metadataResults) > 0 {
log.Error("[identifyDocument] Metadata Error") firstResult := metadataResults[0]
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Metadata Error"})
// Store First Metadata Result
if _, err = api.DB.Queries.AddMetadata(api.DB.Ctx, database.AddMetadataParams{
DocumentID: rDocID.DocumentID,
Title: firstResult.Title,
Author: firstResult.Author,
Description: firstResult.Description,
Gbid: firstResult.GBID,
Olid: firstResult.OLID,
Isbn10: firstResult.ISBN10,
Isbn13: firstResult.ISBN13,
}); err != nil {
log.Error("[identifyDocument] AddMetadata DB Error:", err)
}
templateVars["Metadata"] = firstResult
} else {
log.Warn("[identifyDocument] Metadata Error")
templateVars["MetadataError"] = "No Metadata Found"
}
document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{
UserID: rUser.(string),
DocumentID: rDocID.DocumentID,
})
if err != nil {
log.Error("[identifyDocument] GetDocumentWithStats DB Error:", err)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return return
} }
// TODO templateVars["Data"] = document
firstResult := metadataResults[0]
if firstResult.Title != nil { c.HTML(http.StatusOK, "document", templateVars)
log.Info("Title:", *firstResult.Title)
}
if firstResult.Author != nil {
log.Info("Author:", *firstResult.Author)
}
if firstResult.Description != nil {
log.Info("Description:", *firstResult.Description)
}
if firstResult.ISBN10 != nil {
log.Info("ISBN 10:", *firstResult.ISBN10)
}
if firstResult.ISBN13 != nil {
log.Info("ISBN 13:", *firstResult.ISBN13)
}
c.Redirect(http.StatusFound, "/")
} }

View File

@ -85,6 +85,7 @@ func (api *API) authFormLogin(c *gin.Context) {
if username == "" || rawPassword == "" { if username == "" || rawPassword == "" {
c.HTML(http.StatusUnauthorized, "login", gin.H{ c.HTML(http.StatusUnauthorized, "login", gin.H{
"RegistrationEnabled": api.Config.RegistrationEnabled,
"Error": "Invalid Credentials", "Error": "Invalid Credentials",
}) })
return return
@ -93,6 +94,7 @@ func (api *API) authFormLogin(c *gin.Context) {
if authorized := api.authorizeCredentials(username, password); authorized != true { if authorized := api.authorizeCredentials(username, password); authorized != true {
c.HTML(http.StatusUnauthorized, "login", gin.H{ c.HTML(http.StatusUnauthorized, "login", gin.H{
"RegistrationEnabled": api.Config.RegistrationEnabled,
"Error": "Invalid Credentials", "Error": "Invalid Credentials",
}) })
return return

View File

@ -380,8 +380,12 @@ current_streak AS (
end_date AS current_streak_end_date end_date AS current_streak_end_date
FROM streaks FROM streaks
WHERE CASE WHERE CASE
WHEN ?2 = "WEEK" THEN DATE('now', time_offset, 'weekday 0', '-7 day') = current_streak_end_date WHEN ?2 = "WEEK" THEN
WHEN ?2 = "DAY" THEN DATE('now', time_offset, '-1 day') = current_streak_end_date OR DATE('now', time_offset) = current_streak_end_date DATE('now', time_offset, 'weekday 0', '-14 day') = current_streak_end_date
OR DATE('now', time_offset, 'weekday 0', '-7 day') = current_streak_end_date
WHEN ?2 = "DAY" THEN
DATE('now', time_offset, '-1 day') = current_streak_end_date
OR DATE('now', time_offset) = current_streak_end_date
END END
LIMIT 1 LIMIT 1
) )

View File

@ -1006,8 +1006,12 @@ current_streak AS (
end_date AS current_streak_end_date end_date AS current_streak_end_date
FROM streaks FROM streaks
WHERE CASE WHERE CASE
WHEN ?2 = "WEEK" THEN DATE('now', time_offset, 'weekday 0', '-7 day') = current_streak_end_date WHEN ?2 = "WEEK" THEN
WHEN ?2 = "DAY" THEN DATE('now', time_offset, '-1 day') = current_streak_end_date OR DATE('now', time_offset) = current_streak_end_date DATE('now', time_offset, 'weekday 0', '-14 day') = current_streak_end_date
OR DATE('now', time_offset, 'weekday 0', '-7 day') = current_streak_end_date
WHEN ?2 = "DAY" THEN
DATE('now', time_offset, '-1 day') = current_streak_end_date
OR DATE('now', time_offset) = current_streak_end_date
END END
LIMIT 1 LIMIT 1
) )

View File

@ -129,7 +129,7 @@ func GetMetadata(metadataSearch MetadataInfo) ([]MetadataInfo, error) {
return allMetadata, nil return allMetadata, nil
} }
func SaveCover(gbid string, coverDir string, documentID string) (*string, error) { func SaveCover(gbid string, coverDir string, documentID string, overwrite bool) (*string, error) {
// Google Books -> JPG // Google Books -> JPG
coverFile := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", documentID)) coverFile := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", documentID))
@ -137,7 +137,7 @@ func SaveCover(gbid string, coverDir string, documentID string) (*string, error)
// Validate File Doesn't Exists // Validate File Doesn't Exists
_, err := os.Stat(coverFilePath) _, err := os.Stat(coverFilePath)
if err == nil { if err == nil && overwrite == false {
log.Warn("[SaveCover] File Alreads Exists") log.Warn("[SaveCover] File Alreads Exists")
return &coverFile, nil return &coverFile, nil
} }

11
screenshots/README.md Normal file
View File

@ -0,0 +1,11 @@
# Screenshots
## Process Images
```bash
# Resize
sips -Z 1500 *.png
# Crop Top & Bottom
sips --cropOffset 85 1 -c 1385 693 *.png
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

BIN
screenshots/pwa/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

BIN
screenshots/pwa/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 KiB

BIN
screenshots/web/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
screenshots/web/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

View File

@ -1,9 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<link rel="manifest" href="./manifest.json" /> <link rel="manifest" href="{{ .RelBase }}./manifest.json" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport"
content="width=device-width, initial-scale=0.90, user-scalable=no">
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<title>Book Manager - {{block "title" .}}{{end}}</title> <title>Book Manager - {{block "title" .}}{{end}}</title>
</head> </head>
@ -11,33 +12,18 @@
<main <main
class="relative h-screen overflow-hidden bg-gray-100 dark:bg-gray-800" class="relative h-screen overflow-hidden bg-gray-100 dark:bg-gray-800"
> >
<div class="flex items-start justify-between"> <div class="flex items-center justify-between w-full h-16">
<input type="checkbox" id="mobile-nav-button" class="hidden"/> <div id="mobile-nav-button" class="flex flex-col z-40 relative ml-6">
<div class="fixed -left-64 duration-500 transition-all w-56 z-50 h-screen shadow-lg lg:left-0 lg:block lg:relative"> <input type="checkbox" class="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0 w-12 h-12" />
<div class="h-full bg-white dark:bg-gray-700"> <span class="lg:hidden w-7 h-0.5 z-40 mt-0.5 bg-white"></span>
<div class="flex items-center justify-center gap-4 h-16"> <span class="lg:hidden w-7 h-0.5 z-40 mt-1 bg-white"></span>
<label <span class="lg:hidden w-7 h-0.5 z-40 mt-1 bg-white"></span>
id="mobile-nav-close-button"
for="mobile-nav-button" <div id="menu" class="fixed -mt-6 -ml-6 lg:-mt-8 h-full w-56 lg:w-48 bg-white dark:bg-gray-700 shadow-lg">
class="flex block items-center p-2 text-gray-500 bg-white rounded-full shadow text-md cursor-pointer lg:hidden" <div class="h-16 flex justify-end lg:justify-around">
> <p class="text-xl font-bold dark:text-white text-right my-auto pr-4 lg:pr-0">Book Manager</p>
<svg
width="20"
height="20"
class="text-gray-400"
fill="currentColor"
viewBox="0 0 1792 1792"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1664 1344v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45z"
></path>
</svg>
</label>
<p class="text-xl font-bold dark:text-white">Book Manager</p>
</div> </div>
<nav class="mt-6"> <div class="mt-6">
<div>
<a <a
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "home"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}" class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "home"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
href="/" href="/"
@ -114,30 +100,8 @@
<span class="mx-4 text-sm font-normal"> Graphs </span> <span class="mx-4 text-sm font-normal"> Graphs </span>
</a> </a>
</div> </div>
</nav>
</div> </div>
</div> </div>
<div class="flex flex-col w-full">
<header class="z-40 flex items-center justify-between w-full h-16">
<div class="block ml-6 lg:hidden">
<label
for="mobile-nav-button"
class="flex items-center p-2 text-gray-500 bg-white rounded-full shadow text-md cursor-pointer"
>
<svg
width="20"
height="20"
class="text-gray-400"
fill="currentColor"
viewBox="0 0 1792 1792"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1664 1344v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45z"
></path>
</svg>
</label>
</div>
<h1 class="text-xl font-bold dark:text-white px-6">{{block "header" .}}{{end}}</h1> <h1 class="text-xl font-bold dark:text-white px-6">{{block "header" .}}{{end}}</h1>
<div <div
class="relative flex items-center justify-end w-full p-4 space-x-4" class="relative flex items-center justify-end w-full p-4 space-x-4"
@ -159,7 +123,7 @@
<input type="checkbox" id="user-dropdown-button" class="hidden"/> <input type="checkbox" id="user-dropdown-button" class="hidden"/>
<div <div
id="user-dropdown" id="user-dropdown"
class="transition duration-200 absolute right-4 top-16 pt-4" class="transition duration-200 z-20 absolute right-4 top-16 pt-4"
> >
<div <div
class="w-56 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5" class="w-56 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5"
@ -202,24 +166,16 @@
</div> </div>
</label> </label>
</div> </div>
</header> </div>
<div class="h-screen px-4 pb-24 overflow-auto md:px-6">
<div class="h-screen px-4 pb-24 overflow-auto md:px-6 lg:ml-48">
{{block "content" .}}{{end}} {{block "content" .}}{{end}}
</div> </div>
</div>
</div>
</main> </main>
<!-- Custom Animation CSS --> <!-- Custom Animation CSS -->
<style> <style>
/* ----------------------------- */
/* ------ Navigation Slide ----- */
/* ----------------------------- */
#mobile-nav-button:checked + div {
left: 0px;
}
/* ----------------------------- */ /* ----------------------------- */
/* ------- User Dropdown ------- */ /* ------- User Dropdown ------- */
/* ----------------------------- */ /* ----------------------------- */
@ -232,6 +188,55 @@
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
} }
/* ----------------------------- */
/* ----- Mobile Navigation ----- */
/* ----------------------------- */
#mobile-nav-button span {
transform-origin: 5px 0px;
transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
background 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
opacity 0.55s ease;
}
#mobile-nav-button span:first-child {
transform-origin: 0% 0%;
}
#mobile-nav-button span:nth-last-child(2) {
transform-origin: 0% 100%;
}
#mobile-nav-button input:checked ~ span {
opacity: 1;
transform: rotate(45deg) translate(2px, -2px);
background: #FFFFFF;
}
#mobile-nav-button input:checked ~ span:nth-last-child(3) {
opacity: 0;
transform: rotate(0deg) scale(0.2, 0.2);
}
#mobile-nav-button input:checked ~ span:nth-last-child(2) {
transform: rotate(-45deg) translate(0, 6px);
}
#mobile-nav-button input:checked ~ div {
transform: none;
}
@media (min-width: 1024px) {
#mobile-nav-button input ~ div {
transform: none;
}
}
#menu {
transform-origin: 0% 0%;
transform: translate(-100%, 0);
transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0);
}
</style> </style>
</body> </body>
</html> </html>

View File

@ -3,33 +3,35 @@
{{define "title"}}Documents{{end}} {{define "title"}}Documents{{end}}
{{define "header"}} {{define "header"}}
<a href="../documents">Documents</a> <a href="{{ .RelBase }}./documents">Documents</a>
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<div class="h-full w-full relative">
<!-- Document Info -->
<div class="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white p-4"> <div class="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white p-4">
<div class="flex flex-col gap-2 float-left w-40 md:w-60 lg:w-80 mr-4 mb-2 relative"> <div class="flex flex-col gap-2 float-left w-44 md:w-60 lg:w-80 mr-4 mb-2 relative">
<label class="z-20 cursor-pointer" for="edit-cover-button"> <label class="z-10 cursor-pointer" for="edit-cover-button">
<img class="rounded object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img> <img class="rounded object-fill w-full" src="{{ .RelBase }}./documents/{{.Data.ID}}/cover"></img>
</label> </label>
<div class="flex flex-wrap-reverse justify-between z-40 gap-2 relative"> <div class="flex flex-wrap-reverse justify-between z-20 gap-2 relative">
<div class="mr-2 min-w-[50%]"> <div class="min-w-[50%] md:mr-2">
<div class="flex gap-1 text-sm"> <div class="flex gap-1 text-sm">
<p class="text-gray-400">ISBN 10:</p> <p class="text-gray-500">ISBN-10:</p>
<p class="font-medium"> <p class="font-medium">
{{ or .Data.Isbn10 "N/A" }} {{ or .Data.Isbn10 "N/A" }}
</p> </p>
</div> </div>
<div class="flex gap-1 text-sm"> <div class="flex gap-1 text-sm">
<p class="text-gray-400">ISBN 13:</p> <p class="text-gray-500">ISBN-13:</p>
<p class="font-medium"> <p class="font-medium">
{{ or .Data.Isbn13 "N/A" }} {{ or .Data.Isbn13 "N/A" }}
</p> </p>
</div> </div>
</div> </div>
<div class="flex grow justify-between my-auto text-gray-500 dark:text-gray-400"> <div class="flex grow justify-between my-auto text-gray-500 dark:text-gray-500">
<input type="checkbox" id="edit-cover-button" class="hidden css-button"/> <input type="checkbox" id="edit-cover-button" class="hidden css-button"/>
<div class="absolute z-50 flex flex-col gap-2 top-0 left-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600"> <div class="absolute z-30 flex flex-col gap-2 top-0 left-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form <form
method="POST" method="POST"
enctype="multipart/form-data" enctype="multipart/form-data"
@ -38,10 +40,13 @@
> >
<input <input
type="file" type="file"
id="cover" id="cover_file"
name="cover" name="cover_file"
> >
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Upload Cover</button> <button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Upload Cover</button>
</form> </form>
<form <form
method="POST" method="POST"
@ -49,7 +54,10 @@
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm" class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm"
> >
<input type="checkbox" checked id="remove_cover" name="remove_cover" class="hidden" /> <input type="checkbox" checked id="remove_cover" name="remove_cover" class="hidden" />
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Remove Cover</button> <button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Remove Cover</button>
</form> </form>
</div> </div>
@ -72,13 +80,16 @@
</svg> </svg>
</label> </label>
<input type="checkbox" id="delete-button" class="hidden css-button"/> <input type="checkbox" id="delete-button" class="hidden css-button"/>
<div class="absolute z-50 bottom-7 left-5 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600"> <div class="absolute z-30 bottom-7 left-5 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form <form
method="POST" method="POST"
action="./{{ .Data.ID }}/delete" action="./{{ .Data.ID }}/delete"
class="text-black dark:text-white text-sm" class="text-black dark:text-white text-sm"
> >
<button class="font-medium w-24 px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Delete</button> <button
class="font-medium w-24 px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Delete</button>
</form> </form>
</div> </div>
</div> </div>
@ -113,7 +124,7 @@
</svg> </svg>
</label> </label>
<input type="checkbox" id="edit-button" class="hidden css-button"/> <input type="checkbox" id="edit-button" class="hidden css-button"/>
<div class="absolute z-50 bottom-7 left-5 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600"> <div class="absolute z-30 bottom-7 left-5 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form <form
method="POST" method="POST"
action="./{{ .Data.ID }}/identify" action="./{{ .Data.ID }}/identify"
@ -124,23 +135,29 @@
id="title" id="title"
name="title" name="title"
placeholder="Title" placeholder="Title"
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white" value="{{ or .Data.Title nil }}"
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
> >
<input <input
type="text" type="text"
id="author" id="author"
name="author" name="author"
placeholder="Author" placeholder="Author"
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white" value="{{ or .Data.Author nil }}"
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
> >
<input <input
type="text" type="text"
id="isbn" id="isbn"
name="isbn" name="isbn"
placeholder="ISBN 10 / ISBN 13" placeholder="ISBN 10 / ISBN 13"
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white" value="{{ or .Data.Isbn13 (or .Data.Isbn10 nil) }}"
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
> >
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Identify</button> <button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Identify</button>
</form> </form>
</div> </div>
</div> </div>
@ -180,9 +197,9 @@
</div> </div>
</div> </div>
</div> </div>
<div class="grid grid-cols-2 justify-between gap-4 pb-4"> <div class="grid sm:grid-cols-2 justify-between gap-4 pb-4">
<div class="relative"> <div class="relative">
<div class="text-gray-400 inline-flex gap-2 relative"> <div class="text-gray-500 inline-flex gap-2 relative">
<p>Title</p> <p>Title</p>
<label class="my-auto" for="edit-title-button"> <label class="my-auto" for="edit-title-button">
<svg <svg
@ -205,7 +222,7 @@
</svg> </svg>
</label> </label>
<input type="checkbox" id="edit-title-button" class="hidden css-button"/> <input type="checkbox" id="edit-title-button" class="hidden css-button"/>
<div class="absolute z-50 top-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600"> <div class="absolute z-30 top-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form <form
method="POST" method="POST"
action="./{{ .Data.ID }}/edit" action="./{{ .Data.ID }}/edit"
@ -216,9 +233,12 @@
id="title" id="title"
name="title" name="title"
value="{{ or .Data.Title "N/A" }}" value="{{ or .Data.Title "N/A" }}"
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white" class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
> >
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Save</button> <button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Save</button>
</form> </form>
</div> </div>
</div> </div>
@ -227,7 +247,7 @@
</p> </p>
</div> </div>
<div class="relative"> <div class="relative">
<div class="text-gray-400 inline-flex gap-2 relative"> <div class="text-gray-500 inline-flex gap-2 relative">
<p>Author</p> <p>Author</p>
<label class="my-auto" for="edit-author-button"> <label class="my-auto" for="edit-author-button">
<svg <svg
@ -251,7 +271,7 @@
</label> </label>
<input type="checkbox" id="edit-author-button" class="hidden css-button"/> <input type="checkbox" id="edit-author-button" class="hidden css-button"/>
<div class="absolute z-50 top-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600"> <div class="absolute z-30 top-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form <form
method="POST" method="POST"
action="./{{ .Data.ID }}/edit" action="./{{ .Data.ID }}/edit"
@ -262,9 +282,12 @@
id="author" id="author"
name="author" name="author"
value="{{ or .Data.Author "N/A" }}" value="{{ or .Data.Author "N/A" }}"
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white" class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
> >
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Save</button> <button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Save</button>
</form> </form>
</div> </div>
</div> </div>
@ -273,26 +296,26 @@
</p> </p>
</div> </div>
<div> <div>
<p class="text-gray-400">Time Read</p> <p class="text-gray-500">Time Read</p>
<p class="font-medium text-lg"> <p class="font-medium text-lg">
{{ .Data.TotalTimeMinutes }} Minutes {{ .Data.TotalTimeMinutes }} Minutes
</p> </p>
</div> </div>
<div> <div>
<p class="text-gray-400">Progress</p> <p class="text-gray-500">Progress</p>
<p class="font-medium text-lg"> <p class="font-medium text-lg">
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%) {{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
</p> </p>
</div> </div>
<!-- <!--
<div> <div>
<p class="text-gray-400">ISBN 10</p> <p class="text-gray-500">ISBN 10</p>
<p class="font-medium text-lg"> <p class="font-medium text-lg">
{{ or .Data.Isbn10 "N/A" }} {{ or .Data.Isbn10 "N/A" }}
</p> </p>
</div> </div>
<div> <div>
<p class="text-gray-400">ISBN 13</p> <p class="text-gray-500">ISBN 13</p>
<p class="font-medium text-lg"> <p class="font-medium text-lg">
{{ or .Data.Isbn13 "N/A" }} {{ or .Data.Isbn13 "N/A" }}
</p> </p>
@ -301,7 +324,7 @@
</div> </div>
<div class="relative"> <div class="relative">
<div class="text-gray-400 inline-flex gap-2 relative"> <div class="text-gray-500 inline-flex gap-2 relative">
<p>Description</p> <p>Description</p>
<label class="my-auto" for="edit-description-button"> <label class="my-auto" for="edit-description-button">
<svg <svg
@ -328,9 +351,9 @@
<div class="relative font-medium text-justify hyphens-auto"> <div class="relative font-medium text-justify hyphens-auto">
<input type="checkbox" id="edit-description-button" class="hidden css-button"/> <input type="checkbox" id="edit-description-button" class="hidden css-button"/>
<div <div
class="absolute h-full w-full min-h-[10em] z-50 top-1 right-0 gap-4 flex transition-all duration-200" class="absolute h-full w-full min-h-[10em] z-30 top-1 right-0 gap-4 flex transition-all duration-200"
> >
<img class="hidden md:block invisible rounded w-40 md:w-60 lg:w-80 object-fill" src="../documents/{{.Data.ID}}/cover"></img> <img class="hidden md:block invisible rounded w-44 md:w-60 lg:w-80 object-fill" src="{{ .RelBase }}./documents/{{.Data.ID}}/cover"></img>
<form <form
method="POST" method="POST"
action="./{{ .Data.ID }}/edit" action="./{{ .Data.ID }}/edit"
@ -340,14 +363,122 @@
type="text" type="text"
id="description" id="description"
name="description" name="description"
class="h-full w-full p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white" class="h-full w-full p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
>{{ or .Data.Description "N/A" }}</textarea> >{{ or .Data.Description "N/A" }}</textarea>
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Save</button> <button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Save</button>
</form> </form>
</div> </div>
<p>{{ or .Data.Description "N/A" }}</p> <p>{{ or .Data.Description "N/A" }}</p>
</div> </div>
</div> </div>
{{ if .MetadataError }}
<div class="absolute top-0 left-0 w-full h-full z-50">
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div>
<div class="relative flex flex-col gap-4 p-4 max-h-[95%] bg-white dark:bg-gray-800 overflow-scroll -translate-x-2/4 -translate-y-2/4 top-1/2 left-1/2 w-5/6 overflow-hidden shadow rounded">
<div class="text-center">
<h3 class="text-lg font-bold leading-6 dark:text-gray-300">No Metadata Results Found</h3>
</div>
<a href="{{ .RelBase }}./documents/{{ .Data.ID }}"
class="w-full text-center font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Back to Document</a>
</div>
</div>
{{ end }}
<!-- Metadata Info -->
{{ if .Metadata }}
<div class="absolute top-0 left-0 w-full h-full z-50">
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div>
<div class="relative max-h-[95%] bg-white dark:bg-gray-800 overflow-scroll -translate-x-2/4 -translate-y-2/4 top-1/2 left-1/2 w-5/6 overflow-hidden shadow rounded">
<div class="py-5 text-center">
<h3 class="text-lg font-bold leading-6 dark:text-gray-300">Metadata Results</h3>
</div>
<form
id="metadata-save"
method="POST"
action="{{ .RelBase }}./documents/{{ .Data.ID }}/edit"
class="text-black dark:text-white border-b dark:border-black"
>
<dl>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">
Cover
</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
<img class="rounded object-fill h-32" src="https://books.google.com/books/content/images/frontcover/{{ .Metadata.GBID }}?fife=w480-h690"></img>
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">
Title
</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.Title "N/A" }}
</dd>
</div>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">
Author
</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.Author "N/A" }}
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">
ISBN 10
</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.ISBN10 "N/A" }}
</dd>
</div>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">
ISBN 13
</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.ISBN13 "N/A" }}
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 sm:grid sm:grid-cols-3 sm:gap-4 px-6">
<dt class="my-auto font-medium text-gray-500">
Description
</dt>
<dd class="max-h-[10em] overflow-scroll mt-1 sm:mt-0 sm:col-span-2">
{{ or .Metadata.Description "N/A" }}
</dd>
</div>
</dl>
<div class="hidden">
<input type="text" id="title" name="title" value="{{ .Metadata.Title }}">
<input type="text" id="author" name="author" value="{{ .Metadata.Author }}">
<input type="text" id="description" name="description" value="{{ .Metadata.Description }}">
<input type="text" id="isbn_10" name="isbn_10" value="{{ .Metadata.ISBN10 }}">
<input type="text" id="isbn_13" name="isbn_13" value="{{ .Metadata.ISBN13 }}">
<input type="text" id="cover_gbid" name="cover_gbid" value="{{ .Metadata.GBID }}">
</div>
</form>
<div class="flex justify-end gap-4 m-4">
<a href="{{ .RelBase }}./documents/{{ .Data.ID }}"
class="w-24 text-center font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Cancel</a>
<button
form="metadata-save"
class="w-24 font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Save</button>
</div>
</div>
</div>
{{ end }}
</div>
<style> <style>
.css-button:checked + div { .css-button:checked + div {
visibility: visible; visibility: visible;