[add] docker instructions, [add] metadata gathering, [add] screenshots
38
Dockerfile
@ -1,19 +1,35 @@
|
||||
FROM alpine:edge AS build
|
||||
RUN apk add --no-cache --update go gcc g++
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
# Certificate Store
|
||||
FROM alpine as certs
|
||||
RUN apk update && apk add ca-certificates
|
||||
|
||||
# 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 cp -a ./templates /opt/bookmanager/templates
|
||||
RUN cp -a ./assets /opt/bookmanager/assets
|
||||
|
||||
# Download Dependencies & Compile
|
||||
RUN go mod download
|
||||
RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o /opt/bookmanager/server
|
||||
# Cache Dependencies & Compile
|
||||
ARG TARGETOS
|
||||
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
|
||||
FROM alpine:3.18
|
||||
FROM busybox:1.36
|
||||
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
|
||||
COPY --from=build /opt/bookmanager /opt/bookmanager
|
||||
WORKDIR /opt/bookmanager
|
||||
EXPOSE 8585
|
||||
|
8
Makefile
@ -14,9 +14,13 @@ docker_build_local:
|
||||
docker_build_release_beta:
|
||||
docker buildx build \
|
||||
--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 buildx build \
|
||||
--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 .
|
||||
|
51
README.md
@ -1,14 +1,20 @@
|
||||
# Book Manager
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_login.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_login.png" width="30%">
|
||||
<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/pwa/login.png" width="18%">
|
||||
</a>
|
||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_home.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_home.png" width="30%">
|
||||
<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/pwa/home.png" width="18%">
|
||||
</a>
|
||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_documents.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_documents.png" width="30%">
|
||||
<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/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>
|
||||
</p>
|
||||
|
||||
@ -24,11 +30,30 @@ In additional to the compatible KOSync API's, we add:
|
||||
|
||||
- Additional APIs to automatically upload reading statistics
|
||||
- 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.
|
||||
|
||||
# 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
|
||||
|
||||
| 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):
|
||||
|
||||
```
|
||||
```bash
|
||||
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
~/go/bin/sqlc generate
|
||||
```
|
||||
|
||||
Run Development:
|
||||
|
||||
```
|
||||
CONFIG_PATH=./data DATA_PATH=./data go run cmd/main.go serve
|
||||
```bash
|
||||
CONFIG_PATH=./data DATA_PATH=./data go run main.go serve
|
||||
```
|
||||
|
||||
# Building
|
||||
|
||||
The `Dockerfile` and `Makefile` contain the build information:
|
||||
|
||||
```
|
||||
```bash
|
||||
# Build Local Docker Image
|
||||
make docker_build_local
|
||||
|
||||
@ -75,12 +100,12 @@ make docker_build_release_latest
|
||||
|
||||
If manually building, you must enable CGO:
|
||||
|
||||
```
|
||||
```bash
|
||||
# Download Dependencies
|
||||
go mod download
|
||||
|
||||
# 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
|
||||
|
@ -21,8 +21,11 @@ type requestDocumentEdit struct {
|
||||
Title *string `form:"title"`
|
||||
Author *string `form:"author"`
|
||||
Description *string `form:"description"`
|
||||
ISBN10 *string `form:"isbn_10"`
|
||||
ISBN13 *string `form:"isbn_13"`
|
||||
RemoveCover *string `form:"remove_cover"`
|
||||
CoverFile *multipart.FileHeader `form:"cover"`
|
||||
CoverGBID *string `form:"cover_gbid"`
|
||||
CoverFile *multipart.FileHeader `form:"cover_file"`
|
||||
}
|
||||
|
||||
type requestDocumentIdentify struct {
|
||||
@ -101,6 +104,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["RelBase"] = "../"
|
||||
templateVars["Data"] = document
|
||||
} else if routeName == "activity" {
|
||||
activityFilter := database.GetActivityParams{
|
||||
@ -131,7 +135,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
if err != nil {
|
||||
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()
|
||||
|
||||
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 {
|
||||
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()
|
||||
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()
|
||||
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{
|
||||
"DailyStreak": daily_streak,
|
||||
@ -218,7 +222,7 @@ func (api *API) getDocumentCover(c *gin.Context) {
|
||||
firstResult := metadataResults[0]
|
||||
|
||||
// Save Cover
|
||||
fileName, err := metadata.SaveCover(*firstResult.GBID, coverDir, document.ID)
|
||||
fileName, err := metadata.SaveCover(*firstResult.GBID, coverDir, document.ID, false)
|
||||
if err == nil {
|
||||
coverFile = *fileName
|
||||
}
|
||||
@ -275,8 +279,11 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
if rDocEdit.Author == nil &&
|
||||
rDocEdit.Title == nil &&
|
||||
rDocEdit.Description == nil &&
|
||||
rDocEdit.CoverFile == nil &&
|
||||
rDocEdit.RemoveCover == nil {
|
||||
rDocEdit.ISBN10 == nil &&
|
||||
rDocEdit.ISBN13 == nil &&
|
||||
rDocEdit.RemoveCover == nil &&
|
||||
rDocEdit.CoverGBID == nil &&
|
||||
rDocEdit.CoverFile == nil {
|
||||
log.Error("[createAppResourcesRoute] Missing Form Values")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
@ -288,7 +295,6 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
s := "UNKNOWN"
|
||||
coverFileName = &s
|
||||
} else if rDocEdit.CoverFile != nil {
|
||||
|
||||
// Validate Type & Derive Extension on MIME
|
||||
uploadedFile, err := rDocEdit.CoverFile.Open()
|
||||
if err != nil {
|
||||
@ -325,6 +331,14 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
}
|
||||
|
||||
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
|
||||
@ -333,6 +347,8 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
Title: api.sanitizeInput(rDocEdit.Title),
|
||||
Author: api.sanitizeInput(rDocEdit.Author),
|
||||
Description: api.sanitizeInput(rDocEdit.Description),
|
||||
Isbn10: api.sanitizeInput(rDocEdit.ISBN10),
|
||||
Isbn13: api.sanitizeInput(rDocEdit.ISBN13),
|
||||
Coverfile: coverFileName,
|
||||
}); err != nil {
|
||||
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) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rDocID requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||
log.Error("[identifyDocument] Invalid URI Bind")
|
||||
@ -399,36 +417,52 @@ func (api *API) identifyDocument(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Template Variables
|
||||
templateVars := gin.H{
|
||||
"RelBase": "../../",
|
||||
}
|
||||
|
||||
// Get Metadata
|
||||
metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
|
||||
Title: rDocIdentify.Title,
|
||||
Author: rDocIdentify.Author,
|
||||
ISBN10: rDocIdentify.ISBN,
|
||||
ISBN13: rDocIdentify.ISBN,
|
||||
})
|
||||
if err != nil || len(metadataResults) == 0 {
|
||||
log.Error("[identifyDocument] Metadata Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Metadata Error"})
|
||||
if err == nil && len(metadataResults) > 0 {
|
||||
firstResult := metadataResults[0]
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// TODO
|
||||
firstResult := metadataResults[0]
|
||||
templateVars["Data"] = document
|
||||
|
||||
if firstResult.Title != nil {
|
||||
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, "/")
|
||||
c.HTML(http.StatusOK, "document", templateVars)
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ func (api *API) authFormLogin(c *gin.Context) {
|
||||
|
||||
if username == "" || rawPassword == "" {
|
||||
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
||||
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||
"Error": "Invalid Credentials",
|
||||
})
|
||||
return
|
||||
@ -93,6 +94,7 @@ func (api *API) authFormLogin(c *gin.Context) {
|
||||
|
||||
if authorized := api.authorizeCredentials(username, password); authorized != true {
|
||||
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
||||
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||
"Error": "Invalid Credentials",
|
||||
})
|
||||
return
|
||||
|
@ -380,8 +380,12 @@ current_streak AS (
|
||||
end_date AS current_streak_end_date
|
||||
FROM streaks
|
||||
WHERE CASE
|
||||
WHEN ?2 = "WEEK" THEN 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
|
||||
WHEN ?2 = "WEEK" THEN
|
||||
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
|
||||
LIMIT 1
|
||||
)
|
||||
|
@ -1006,8 +1006,12 @@ current_streak AS (
|
||||
end_date AS current_streak_end_date
|
||||
FROM streaks
|
||||
WHERE CASE
|
||||
WHEN ?2 = "WEEK" THEN 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
|
||||
WHEN ?2 = "WEEK" THEN
|
||||
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
|
||||
LIMIT 1
|
||||
)
|
||||
|
@ -129,7 +129,7 @@ func GetMetadata(metadataSearch MetadataInfo) ([]MetadataInfo, error) {
|
||||
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
|
||||
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
|
||||
_, err := os.Stat(coverFilePath)
|
||||
if err == nil {
|
||||
if err == nil && overwrite == false {
|
||||
log.Warn("[SaveCover] File Alreads Exists")
|
||||
return &coverFile, nil
|
||||
}
|
||||
|
11
screenshots/README.md
Normal 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
|
||||
```
|
BIN
screenshots/pwa/activity.png
Normal file
After Width: | Height: | Size: 229 KiB |
BIN
screenshots/pwa/document.png
Normal file
After Width: | Height: | Size: 586 KiB |
BIN
screenshots/pwa/documents.png
Normal file
After Width: | Height: | Size: 490 KiB |
BIN
screenshots/pwa/home.png
Normal file
After Width: | Height: | Size: 144 KiB |
BIN
screenshots/pwa/login.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
screenshots/pwa/metadata.png
Normal file
After Width: | Height: | Size: 335 KiB |
BIN
screenshots/pwa/navigation.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
screenshots/web/activity.png
Normal file
After Width: | Height: | Size: 245 KiB |
BIN
screenshots/web/document.png
Normal file
After Width: | Height: | Size: 639 KiB |
BIN
screenshots/web/documents.png
Normal file
After Width: | Height: | Size: 791 KiB |
BIN
screenshots/web/home.png
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
screenshots/web/login.png
Normal file
After Width: | Height: | Size: 790 KiB |
BIN
screenshots/web/metadata.png
Normal file
After Width: | Height: | Size: 357 KiB |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 362 KiB |
Before Width: | Height: | Size: 2.8 MiB |
@ -1,9 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<link rel="manifest" href="{{ .RelBase }}./manifest.json" />
|
||||
<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>
|
||||
<title>Book Manager - {{block "title" .}}{{end}}</title>
|
||||
</head>
|
||||
@ -11,33 +12,18 @@
|
||||
<main
|
||||
class="relative h-screen overflow-hidden bg-gray-100 dark:bg-gray-800"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<input type="checkbox" id="mobile-nav-button" class="hidden"/>
|
||||
<div class="fixed -left-64 duration-500 transition-all w-56 z-50 h-screen shadow-lg lg:left-0 lg:block lg:relative">
|
||||
<div class="h-full bg-white dark:bg-gray-700">
|
||||
<div class="flex items-center justify-center gap-4 h-16">
|
||||
<label
|
||||
id="mobile-nav-close-button"
|
||||
for="mobile-nav-button"
|
||||
class="flex block items-center p-2 text-gray-500 bg-white rounded-full shadow text-md cursor-pointer lg:hidden"
|
||||
>
|
||||
<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 class="flex items-center justify-between w-full h-16">
|
||||
<div id="mobile-nav-button" class="flex flex-col z-40 relative ml-6">
|
||||
<input type="checkbox" class="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0 w-12 h-12" />
|
||||
<span class="lg:hidden w-7 h-0.5 z-40 mt-0.5 bg-white"></span>
|
||||
<span class="lg:hidden w-7 h-0.5 z-40 mt-1 bg-white"></span>
|
||||
<span class="lg:hidden w-7 h-0.5 z-40 mt-1 bg-white"></span>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<nav class="mt-6">
|
||||
<div>
|
||||
<div class="mt-6">
|
||||
<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}}"
|
||||
href="/"
|
||||
@ -114,30 +100,8 @@
|
||||
<span class="mx-4 text-sm font-normal"> Graphs </span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</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>
|
||||
<div
|
||||
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"/>
|
||||
<div
|
||||
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
|
||||
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>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
<div class="h-screen px-4 pb-24 overflow-auto md:px-6">
|
||||
</div>
|
||||
|
||||
<div class="h-screen px-4 pb-24 overflow-auto md:px-6 lg:ml-48">
|
||||
{{block "content" .}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Custom Animation CSS -->
|
||||
<style>
|
||||
|
||||
/* ----------------------------- */
|
||||
/* ------ Navigation Slide ----- */
|
||||
/* ----------------------------- */
|
||||
#mobile-nav-button:checked + div {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
/* ----------------------------- */
|
||||
/* ------- User Dropdown ------- */
|
||||
/* ----------------------------- */
|
||||
@ -232,6 +188,55 @@
|
||||
visibility: hidden;
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -3,33 +3,35 @@
|
||||
{{define "title"}}Documents{{end}}
|
||||
|
||||
{{define "header"}}
|
||||
<a href="../documents">Documents</a>
|
||||
<a href="{{ .RelBase }}./documents">Documents</a>
|
||||
{{end}}
|
||||
|
||||
{{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="flex flex-col gap-2 float-left w-40 md:w-60 lg:w-80 mr-4 mb-2 relative">
|
||||
<label class="z-20 cursor-pointer" for="edit-cover-button">
|
||||
<img class="rounded object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img>
|
||||
<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-10 cursor-pointer" for="edit-cover-button">
|
||||
<img class="rounded object-fill w-full" src="{{ .RelBase }}./documents/{{.Data.ID}}/cover"></img>
|
||||
</label>
|
||||
<div class="flex flex-wrap-reverse justify-between z-40 gap-2 relative">
|
||||
<div class="mr-2 min-w-[50%]">
|
||||
<div class="flex flex-wrap-reverse justify-between z-20 gap-2 relative">
|
||||
<div class="min-w-[50%] md:mr-2">
|
||||
<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">
|
||||
{{ or .Data.Isbn10 "N/A" }}
|
||||
</p>
|
||||
</div>
|
||||
<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">
|
||||
{{ or .Data.Isbn13 "N/A" }}
|
||||
</p>
|
||||
</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"/>
|
||||
<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
|
||||
method="POST"
|
||||
enctype="multipart/form-data"
|
||||
@ -38,10 +40,13 @@
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
id="cover"
|
||||
name="cover"
|
||||
id="cover_file"
|
||||
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
|
||||
method="POST"
|
||||
@ -49,7 +54,10 @@
|
||||
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" />
|
||||
<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>
|
||||
|
||||
</div>
|
||||
@ -72,13 +80,16 @@
|
||||
</svg>
|
||||
</label>
|
||||
<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
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/delete"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@ -113,7 +124,7 @@
|
||||
</svg>
|
||||
</label>
|
||||
<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
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/identify"
|
||||
@ -124,23 +135,29 @@
|
||||
id="title"
|
||||
name="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
|
||||
type="text"
|
||||
id="author"
|
||||
name="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
|
||||
type="text"
|
||||
id="isbn"
|
||||
name="isbn"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@ -180,9 +197,9 @@
|
||||
</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="text-gray-400 inline-flex gap-2 relative">
|
||||
<div class="text-gray-500 inline-flex gap-2 relative">
|
||||
<p>Title</p>
|
||||
<label class="my-auto" for="edit-title-button">
|
||||
<svg
|
||||
@ -205,7 +222,7 @@
|
||||
</svg>
|
||||
</label>
|
||||
<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
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
@ -216,9 +233,12 @@
|
||||
id="title"
|
||||
name="title"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@ -227,7 +247,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
<label class="my-auto" for="edit-author-button">
|
||||
<svg
|
||||
@ -251,7 +271,7 @@
|
||||
</label>
|
||||
<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
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
@ -262,9 +282,12 @@
|
||||
id="author"
|
||||
name="author"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@ -273,26 +296,26 @@
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">Time Read</p>
|
||||
<p class="text-gray-500">Time Read</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ .Data.TotalTimeMinutes }} Minutes
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">Progress</p>
|
||||
<p class="text-gray-500">Progress</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
|
||||
</p>
|
||||
</div>
|
||||
<!--
|
||||
<div>
|
||||
<p class="text-gray-400">ISBN 10</p>
|
||||
<p class="text-gray-500">ISBN 10</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ or .Data.Isbn10 "N/A" }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">ISBN 13</p>
|
||||
<p class="text-gray-500">ISBN 13</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ or .Data.Isbn13 "N/A" }}
|
||||
</p>
|
||||
@ -301,7 +324,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<label class="my-auto" for="edit-description-button">
|
||||
<svg
|
||||
@ -328,9 +351,9 @@
|
||||
<div class="relative font-medium text-justify hyphens-auto">
|
||||
<input type="checkbox" id="edit-description-button" class="hidden css-button"/>
|
||||
<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
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
@ -340,14 +363,122 @@
|
||||
type="text"
|
||||
id="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>
|
||||
<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>
|
||||
</div>
|
||||
<p>{{ or .Data.Description "N/A" }}</p>
|
||||
</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>
|
||||
.css-button:checked + div {
|
||||
visibility: visible;
|
||||
|