Compare commits
28 Commits
6c6a6dd329
...
templ
| Author | SHA1 | Date | |
|---|---|---|---|
| 946e158d25 | |||
| 7ac9c3d573 | |||
|
|
580d64227d | ||
| acf4119d9a | |||
| f6dd8cee50 | |||
| a981d98ba5 | |||
| a193f97d29 | |||
| 841b29c425 | |||
| 3d61d0f5ef | |||
| 5e388730a5 | |||
| 0a1dfeab65 | |||
| d4c8e4d2da | |||
| bbd3a00102 | |||
| 3a633235ea | |||
| 9809a09d2e | |||
| f37bff365f | |||
| 77527bfb05 | |||
| 8de6fed5df | |||
| f9277d3b32 | |||
| db9629a618 | |||
| 546600db93 | |||
| 7c6acad689 | |||
| 5482899075 | |||
| 5a64ff7029 | |||
| a7ecb1a6f8 | |||
| 2d206826d6 | |||
| f1414e3e4e | |||
| 8e81acd381 |
@@ -2,6 +2,10 @@ kind: pipeline
|
||||
type: kubernetes
|
||||
name: default
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
|
||||
steps:
|
||||
# Unit Tests
|
||||
- name: tests
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ data/
|
||||
build/
|
||||
.direnv/
|
||||
cover.html
|
||||
node_modules
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Certificate Store
|
||||
FROM alpine AS certs
|
||||
RUN apk update && apk add ca-certificates
|
||||
# Certificates & Timezones
|
||||
FROM alpine AS alpine
|
||||
RUN apk update && apk add --no-cache ca-certificates tzdata
|
||||
|
||||
# Build Image
|
||||
FROM golang:1.21 AS build
|
||||
@@ -19,7 +19,8 @@ RUN go build \
|
||||
|
||||
# Create Image
|
||||
FROM busybox:1.36
|
||||
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
|
||||
COPY --from=alpine /etc/ssl/certs /etc/ssl/certs
|
||||
COPY --from=alpine /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
COPY --from=build /opt/antholume /opt/antholume
|
||||
WORKDIR /opt/antholume
|
||||
EXPOSE 8585
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Certificate Store
|
||||
FROM alpine AS certs
|
||||
RUN apk update && apk add ca-certificates
|
||||
# Certificates & Timezones
|
||||
FROM alpine AS alpine
|
||||
RUN apk update && apk add --no-cache ca-certificates tzdata
|
||||
|
||||
# Build Image
|
||||
FROM --platform=$BUILDPLATFORM golang:1.21 AS build
|
||||
@@ -21,7 +21,8 @@ RUN --mount=target=. \
|
||||
|
||||
# Create Image
|
||||
FROM busybox:1.36
|
||||
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
|
||||
COPY --from=alpine /etc/ssl/certs /etc/ssl/certs
|
||||
COPY --from=alpine /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
COPY --from=build /opt/antholume /opt/antholume
|
||||
WORKDIR /opt/antholume
|
||||
EXPOSE 8585
|
||||
|
||||
@@ -118,7 +118,7 @@ See documentation in the `client` subfolder: [SyncNinja](https://gitea.va.reicha
|
||||
|
||||
## Development
|
||||
|
||||
SQLC Generation (v1.21.0):
|
||||
SQLC Generation (v1.26.0):
|
||||
|
||||
```bash
|
||||
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
|
||||
125
api/api.go
125
api/api.go
@@ -6,6 +6,7 @@ import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -15,7 +16,9 @@ import (
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/antholume/api/renderer"
|
||||
"reichard.io/antholume/config"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/utils"
|
||||
@@ -37,12 +40,17 @@ func NewApi(db *database.DBManager, c *config.Config, assets fs.FS) *API {
|
||||
db: db,
|
||||
cfg: c,
|
||||
assets: assets,
|
||||
templates: make(map[string]*template.Template),
|
||||
userAuthCache: make(map[string]string),
|
||||
}
|
||||
|
||||
// Create router
|
||||
router := gin.New()
|
||||
|
||||
// Override renderer
|
||||
ginRenderer := router.HTMLRender
|
||||
router.HTMLRender = &renderer.HTMLTemplRenderer{FallbackHtmlRenderer: ginRenderer}
|
||||
|
||||
// Add server
|
||||
api.httpServer = &http.Server{
|
||||
Handler: router,
|
||||
@@ -157,6 +165,7 @@ func (api *API) registerWebAppRoutes(router *gin.Engine) {
|
||||
router.GET("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminImport)
|
||||
router.POST("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminImport)
|
||||
router.GET("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminUsers)
|
||||
router.POST("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appUpdateAdminUsers)
|
||||
router.GET("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdmin)
|
||||
router.POST("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminAction)
|
||||
router.POST("/login", api.appAuthLogin)
|
||||
@@ -222,67 +231,105 @@ func (api *API) registerOPDSRoutes(apiGroup *gin.RouterGroup) {
|
||||
|
||||
func (api *API) generateTemplates() *multitemplate.Renderer {
|
||||
// Define templates & helper functions
|
||||
templates := make(map[string]*template.Template)
|
||||
render := multitemplate.NewRenderer()
|
||||
templates := make(map[string]*template.Template)
|
||||
helperFuncs := template.FuncMap{
|
||||
"dict": dict,
|
||||
"slice": slice,
|
||||
"fields": fields,
|
||||
"getSVGGraphData": getSVGGraphData,
|
||||
"getUTCOffsets": getUTCOffsets,
|
||||
"getTimeZones": getTimeZones,
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
"niceNumbers": niceNumbers,
|
||||
"niceSeconds": niceSeconds,
|
||||
}
|
||||
|
||||
// Load base
|
||||
b, _ := fs.ReadFile(api.assets, "templates/base.tmpl")
|
||||
baseTemplate := template.Must(template.New("base").Funcs(helperFuncs).Parse(string(b)))
|
||||
// Load Base
|
||||
b, err := fs.ReadFile(api.assets, "templates/base.tmpl")
|
||||
if err != nil {
|
||||
log.Errorf("error reading base template: %v", err)
|
||||
return &render
|
||||
}
|
||||
|
||||
// Parse Base
|
||||
baseTemplate, err := template.New("base").Funcs(helperFuncs).Parse(string(b))
|
||||
if err != nil {
|
||||
log.Errorf("error parsing base template: %v", err)
|
||||
return &render
|
||||
}
|
||||
|
||||
// Load SVGs
|
||||
svgs, _ := fs.ReadDir(api.assets, "templates/svgs")
|
||||
for _, item := range svgs {
|
||||
basename := item.Name()
|
||||
path := fmt.Sprintf("templates/svgs/%s", basename)
|
||||
name := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
|
||||
b, _ := fs.ReadFile(api.assets, path)
|
||||
baseTemplate = template.Must(baseTemplate.New("svg/" + name).Parse(string(b)))
|
||||
templates["svg/"+name] = baseTemplate
|
||||
err = api.loadTemplates("svg", baseTemplate, templates, false)
|
||||
if err != nil {
|
||||
log.Errorf("error loading svg templates: %v", err)
|
||||
return &render
|
||||
}
|
||||
|
||||
// Load components
|
||||
components, _ := fs.ReadDir(api.assets, "templates/components")
|
||||
for _, item := range components {
|
||||
basename := item.Name()
|
||||
path := fmt.Sprintf("templates/components/%s", basename)
|
||||
name := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
|
||||
// Clone Base Template
|
||||
b, _ := fs.ReadFile(api.assets, path)
|
||||
baseTemplate = template.Must(baseTemplate.New("component/" + name).Parse(string(b)))
|
||||
render.Add("component/"+name, baseTemplate)
|
||||
templates["component/"+name] = baseTemplate
|
||||
// Load Components
|
||||
err = api.loadTemplates("component", baseTemplate, templates, false)
|
||||
if err != nil {
|
||||
log.Errorf("error loading component templates: %v", err)
|
||||
return &render
|
||||
}
|
||||
|
||||
// Load pages
|
||||
pages, _ := fs.ReadDir(api.assets, "templates/pages")
|
||||
for _, item := range pages {
|
||||
basename := item.Name()
|
||||
path := fmt.Sprintf("templates/pages/%s", basename)
|
||||
name := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
|
||||
// Clone Base Template
|
||||
b, _ := fs.ReadFile(api.assets, path)
|
||||
pageTemplate, _ := template.Must(baseTemplate.Clone()).New("page/" + name).Parse(string(b))
|
||||
render.Add("page/"+name, pageTemplate)
|
||||
templates["page/"+name] = pageTemplate
|
||||
// Load Pages
|
||||
err = api.loadTemplates("page", baseTemplate, templates, true)
|
||||
if err != nil {
|
||||
log.Errorf("error loading page templates: %v", err)
|
||||
return &render
|
||||
}
|
||||
|
||||
// Populate Renderer
|
||||
api.templates = templates
|
||||
for templateName, templateValue := range templates {
|
||||
render.Add(templateName, templateValue)
|
||||
}
|
||||
|
||||
return &render
|
||||
}
|
||||
|
||||
func (api *API) loadTemplates(
|
||||
basePath string,
|
||||
baseTemplate *template.Template,
|
||||
allTemplates map[string]*template.Template,
|
||||
cloneBase bool,
|
||||
) error {
|
||||
// Load Templates (Pluralize)
|
||||
templateDirectory := fmt.Sprintf("templates/%ss", basePath)
|
||||
allFiles, err := fs.ReadDir(api.assets, templateDirectory)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("unable to read template dir: %s", templateDirectory))
|
||||
}
|
||||
|
||||
// Generate Templates
|
||||
for _, item := range allFiles {
|
||||
templateFile := item.Name()
|
||||
templatePath := path.Join(templateDirectory, templateFile)
|
||||
templateName := fmt.Sprintf("%s/%s", basePath, strings.TrimSuffix(templateFile, filepath.Ext(templateFile)))
|
||||
|
||||
// Read Template
|
||||
b, err := fs.ReadFile(api.assets, templatePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("unable to read template: %s", templateName))
|
||||
}
|
||||
|
||||
// Clone? (Pages - Don't Stomp)
|
||||
if cloneBase {
|
||||
baseTemplate = template.Must(baseTemplate.Clone())
|
||||
}
|
||||
|
||||
// Parse Template
|
||||
baseTemplate, err = baseTemplate.New(templateName).Parse(string(b))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("unable to parse template: %s", templateName))
|
||||
}
|
||||
|
||||
allTemplates[templateName] = baseTemplate
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loggingMiddleware(c *gin.Context) {
|
||||
// Start timer
|
||||
startTime := time.Now()
|
||||
@@ -298,7 +345,7 @@ func loggingMiddleware(c *gin.Context) {
|
||||
logData := log.Fields{
|
||||
"type": "access",
|
||||
"ip": c.ClientIP(),
|
||||
"latency": fmt.Sprintf("%s", latency),
|
||||
"latency": latency.String(),
|
||||
"status": c.Writer.Status(),
|
||||
"method": c.Request.Method,
|
||||
"path": c.Request.URL.Path,
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -12,14 +13,19 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
argon2 "github.com/alexedwards/argon2id"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/itchyny/gojq"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/metadata"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
type adminAction string
|
||||
@@ -54,10 +60,41 @@ type requestAdminImport struct {
|
||||
Type importType `form:"type"`
|
||||
}
|
||||
|
||||
type operationType string
|
||||
|
||||
const (
|
||||
opUpdate operationType = "UPDATE"
|
||||
opCreate operationType = "CREATE"
|
||||
opDelete operationType = "DELETE"
|
||||
)
|
||||
|
||||
type requestAdminUpdateUser struct {
|
||||
User string `form:"user"`
|
||||
Password *string `form:"password"`
|
||||
IsAdmin *bool `form:"is_admin"`
|
||||
Operation operationType `form:"operation"`
|
||||
}
|
||||
|
||||
type requestAdminLogs struct {
|
||||
Filter string `form:"filter"`
|
||||
}
|
||||
|
||||
type importStatus string
|
||||
|
||||
const (
|
||||
importFailed importStatus = "FAILED"
|
||||
importSuccess importStatus = "SUCCESS"
|
||||
importExists importStatus = "EXISTS"
|
||||
)
|
||||
|
||||
type importResult struct {
|
||||
ID string
|
||||
Name string
|
||||
Path string
|
||||
Status importStatus
|
||||
Error error
|
||||
}
|
||||
|
||||
func (api *API) appPerformAdminAction(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("admin", c)
|
||||
|
||||
@@ -68,15 +105,18 @@ func (api *API) appPerformAdminAction(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO - Messages
|
||||
switch rAdminAction.Action {
|
||||
case adminMetadataMatch:
|
||||
// TODO
|
||||
// 1. Documents xref most recent metadata table?
|
||||
// 2. Select all / deselect?
|
||||
case adminCacheTables:
|
||||
go api.db.CacheTempTables()
|
||||
// TODO - Message
|
||||
go func() {
|
||||
err := api.db.CacheTempTables()
|
||||
if err != nil {
|
||||
log.Error("Unable to cache temp tables: ", err)
|
||||
}
|
||||
}()
|
||||
case adminRestore:
|
||||
api.processRestoreFile(rAdminAction, c)
|
||||
return
|
||||
@@ -134,7 +174,10 @@ func (api *API) appGetAdminLogs(c *gin.Context) {
|
||||
rAdminLogs.Filter = strings.TrimSpace(rAdminLogs.Filter)
|
||||
|
||||
var jqFilter *gojq.Code
|
||||
if rAdminLogs.Filter != "" {
|
||||
var basicFilter string
|
||||
if strings.HasPrefix(rAdminLogs.Filter, "\"") && strings.HasSuffix(rAdminLogs.Filter, "\"") {
|
||||
basicFilter = rAdminLogs.Filter[1 : len(rAdminLogs.Filter)-1]
|
||||
} else if rAdminLogs.Filter != "" {
|
||||
parsed, err := gojq.Parse(rAdminLogs.Filter)
|
||||
if err != nil {
|
||||
log.Error("Unable to parse JQ filter")
|
||||
@@ -166,7 +209,7 @@ func (api *API) appGetAdminLogs(c *gin.Context) {
|
||||
rawLog := scanner.Text()
|
||||
|
||||
// Attempt JSON Pretty
|
||||
var jsonMap map[string]interface{}
|
||||
var jsonMap map[string]any
|
||||
err := json.Unmarshal([]byte(rawLog), &jsonMap)
|
||||
if err != nil {
|
||||
logLines = append(logLines, scanner.Text())
|
||||
@@ -180,12 +223,17 @@ func (api *API) appGetAdminLogs(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
// No Filter
|
||||
if jqFilter == nil {
|
||||
// Basic Filter
|
||||
if basicFilter != "" && strings.Contains(string(rawData), basicFilter) {
|
||||
logLines = append(logLines, string(rawData))
|
||||
continue
|
||||
}
|
||||
|
||||
// No JQ Filter
|
||||
if jqFilter == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Error or nil
|
||||
result, _ := jqFilter.Run(jsonMap).Next()
|
||||
if _, ok := result.(error); ok {
|
||||
@@ -225,6 +273,52 @@ func (api *API) appGetAdminUsers(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "page/admin-users", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appUpdateAdminUsers(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("admin-users", c)
|
||||
|
||||
var rUpdate requestAdminUpdateUser
|
||||
if err := c.ShouldBind(&rUpdate); err != nil {
|
||||
log.Error("Invalid URI Bind")
|
||||
appErrorPage(c, http.StatusNotFound, "Invalid user parameters")
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure Username
|
||||
if rUpdate.User == "" {
|
||||
appErrorPage(c, http.StatusInternalServerError, "User cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
switch rUpdate.Operation {
|
||||
case opCreate:
|
||||
err = api.createUser(rUpdate.User, rUpdate.Password, rUpdate.IsAdmin)
|
||||
case opUpdate:
|
||||
err = api.updateUser(rUpdate.User, rUpdate.Password, rUpdate.IsAdmin)
|
||||
case opDelete:
|
||||
err = api.deleteUser(rUpdate.User)
|
||||
default:
|
||||
appErrorPage(c, http.StatusNotFound, "Unknown user operation")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unable to create or update user: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
users, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||
if err != nil {
|
||||
log.Error("GetUsers DB Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUsers DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["Data"] = users
|
||||
|
||||
c.HTML(http.StatusOK, "page/admin-users", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetAdminImport(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("admin-import", c)
|
||||
|
||||
@@ -285,46 +379,157 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO - Store results for approval?
|
||||
|
||||
// Walk import directory & copy or import files
|
||||
// Get import directory
|
||||
importDirectory := filepath.Clean(rAdminImport.Directory)
|
||||
_ = filepath.WalkDir(importDirectory, func(currentPath string, f fs.DirEntry, err error) error {
|
||||
|
||||
// Get data directory
|
||||
absoluteDataPath, _ := filepath.Abs(filepath.Join(api.cfg.DataPath, "documents"))
|
||||
|
||||
// Validate different path
|
||||
if absoluteDataPath == importDirectory {
|
||||
appErrorPage(c, http.StatusBadRequest, "Directory is the same as data path")
|
||||
return
|
||||
}
|
||||
|
||||
// Do Transaction
|
||||
tx, err := api.db.DB.Begin()
|
||||
if err != nil {
|
||||
log.Error("Transaction Begin DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown error")
|
||||
return
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
// Track imports
|
||||
importResults := make([]importResult, 0)
|
||||
|
||||
// Walk Directory & Import
|
||||
err = filepath.WalkDir(importDirectory, func(importPath string, f fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get metadata
|
||||
fileMeta, err := metadata.GetMetadata(currentPath)
|
||||
// Get relative path
|
||||
basePath := importDirectory
|
||||
relFilePath, err := filepath.Rel(importDirectory, importPath)
|
||||
if err != nil {
|
||||
fmt.Printf("metadata error: %v\n", err)
|
||||
log.Warnf("path error: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only needed if copying
|
||||
newName := deriveBaseFileName(fileMeta)
|
||||
// Track imports
|
||||
iResult := importResult{
|
||||
Path: relFilePath,
|
||||
Status: importFailed,
|
||||
}
|
||||
defer func() {
|
||||
importResults = append(importResults, iResult)
|
||||
}()
|
||||
|
||||
// Open File on Disk
|
||||
// file, err := os.Open(currentPath)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer file.Close()
|
||||
// Get metadata
|
||||
fileMeta, err := metadata.GetMetadata(importPath)
|
||||
if err != nil {
|
||||
log.Errorf("metadata error: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
iResult.ID = *fileMeta.PartialMD5
|
||||
iResult.Name = fmt.Sprintf("%s - %s", *fileMeta.Author, *fileMeta.Title)
|
||||
|
||||
// TODO - BasePath in DB
|
||||
// TODO - Copy / Import
|
||||
// Check already exists
|
||||
_, err = qtx.GetDocument(api.db.Ctx, *fileMeta.PartialMD5)
|
||||
if err == nil {
|
||||
log.Warnf("document already exists: %s", *fileMeta.PartialMD5)
|
||||
iResult.Status = importExists
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("New File Metadata: %s\n", newName)
|
||||
// Import Copy
|
||||
if rAdminImport.Type == importCopy {
|
||||
// Derive & Sanitize File Name
|
||||
relFilePath = deriveBaseFileName(fileMeta)
|
||||
safePath := filepath.Join(api.cfg.DataPath, "documents", relFilePath)
|
||||
|
||||
// Open Source File
|
||||
srcFile, err := os.Open(importPath)
|
||||
if err != nil {
|
||||
log.Errorf("unable to open current file: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Open Destination File
|
||||
destFile, err := os.Create(safePath)
|
||||
if err != nil {
|
||||
log.Errorf("unable to open destination file: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// Copy File
|
||||
if _, err = io.Copy(destFile, srcFile); err != nil {
|
||||
log.Errorf("unable to save file: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update Base & Path
|
||||
basePath = filepath.Join(api.cfg.DataPath, "documents")
|
||||
iResult.Path = relFilePath
|
||||
}
|
||||
|
||||
// Upsert document
|
||||
if _, err = qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||
ID: *fileMeta.PartialMD5,
|
||||
Title: fileMeta.Title,
|
||||
Author: fileMeta.Author,
|
||||
Description: fileMeta.Description,
|
||||
Md5: fileMeta.MD5,
|
||||
Words: fileMeta.WordCount,
|
||||
Filepath: &relFilePath,
|
||||
Basepath: &basePath,
|
||||
}); err != nil {
|
||||
log.Errorf("UpsertDocument DB Error: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
|
||||
iResult.Status = importSuccess
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Import Failed: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["CurrentPath"] = filepath.Clean(rAdminImport.Directory)
|
||||
// Commit transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Error("Transaction Commit DB Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Import DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "page/admin-import", templateVars)
|
||||
// Sort import results
|
||||
sort.Slice(importResults, func(i int, j int) bool {
|
||||
return importStatusPriority(importResults[i].Status) <
|
||||
importStatusPriority(importResults[j].Status)
|
||||
})
|
||||
|
||||
templateVars["Data"] = importResults
|
||||
c.HTML(http.StatusOK, "page/admin-import-results", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Context) {
|
||||
@@ -420,14 +625,6 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
||||
}
|
||||
defer backupFile.Close()
|
||||
|
||||
// Vacuum DB
|
||||
_, err = api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
||||
if err != nil {
|
||||
log.Error("Unable to vacuum DB: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
|
||||
return
|
||||
}
|
||||
|
||||
// Save Backup File
|
||||
w := bufio.NewWriter(backupFile)
|
||||
err = api.createBackup(w, []string{"covers", "documents"})
|
||||
@@ -468,7 +665,6 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
}
|
||||
|
||||
// Restore all data
|
||||
func (api *API) restoreData(zipReader *zip.Reader) error {
|
||||
// Ensure Directories
|
||||
api.cfg.EnsureDirectories()
|
||||
@@ -484,14 +680,14 @@ func (api *API) restoreData(zipReader *zip.Reader) error {
|
||||
destPath := filepath.Join(api.cfg.DataPath, file.Name)
|
||||
destFile, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating destination file:", err)
|
||||
log.Errorf("error creating destination file: %v", err)
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// Copy the contents from the zip file to the destination file.
|
||||
if _, err := io.Copy(destFile, rc); err != nil {
|
||||
fmt.Println("Error copying file contents:", err)
|
||||
log.Errorf("Error copying file contents: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -499,7 +695,6 @@ func (api *API) restoreData(zipReader *zip.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove all data
|
||||
func (api *API) removeData() error {
|
||||
allPaths := []string{
|
||||
"covers",
|
||||
@@ -522,10 +717,14 @@ func (api *API) removeData() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Backup all data
|
||||
func (api *API) createBackup(w io.Writer, directories []string) error {
|
||||
ar := zip.NewWriter(w)
|
||||
// Vacuum DB
|
||||
_, err := api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to vacuum database")
|
||||
}
|
||||
|
||||
ar := zip.NewWriter(w)
|
||||
exportWalker := func(currentPath string, f fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -575,7 +774,11 @@ func (api *API) createBackup(w io.Writer, directories []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
io.Copy(newDbFile, dbFile)
|
||||
|
||||
_, err = io.Copy(newDbFile, dbFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Backup Covers & Documents
|
||||
for _, dir := range directories {
|
||||
@@ -588,3 +791,159 @@ func (api *API) createBackup(w io.Writer, directories []string) error {
|
||||
ar.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) isLastAdmin(userID string) (bool, error) {
|
||||
allUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, fmt.Sprintf("GetUsers DB Error: %v", err))
|
||||
}
|
||||
|
||||
hasAdmin := false
|
||||
for _, user := range allUsers {
|
||||
if user.Admin && user.ID != userID {
|
||||
hasAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return !hasAdmin, nil
|
||||
}
|
||||
|
||||
func (api *API) createUser(user string, rawPassword *string, isAdmin *bool) error {
|
||||
// Validate Necessary Parameters
|
||||
if rawPassword == nil || *rawPassword == "" {
|
||||
return fmt.Errorf("password can't be empty")
|
||||
}
|
||||
|
||||
// Base Params
|
||||
createParams := database.CreateUserParams{
|
||||
ID: user,
|
||||
}
|
||||
|
||||
// Handle Admin (Explicit or False)
|
||||
if isAdmin != nil {
|
||||
createParams.Admin = *isAdmin
|
||||
} else {
|
||||
createParams.Admin = false
|
||||
}
|
||||
|
||||
// Parse Password
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword)))
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create hashed password")
|
||||
}
|
||||
createParams.Pass = &hashedPassword
|
||||
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create token for user")
|
||||
}
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
createParams.AuthHash = &authHash
|
||||
|
||||
// Create user in DB
|
||||
if rows, err := api.db.Queries.CreateUser(api.db.Ctx, createParams); err != nil {
|
||||
log.Error("CreateUser DB Error:", err)
|
||||
return fmt.Errorf("unable to create user")
|
||||
} else if rows == 0 {
|
||||
log.Warn("User Already Exists:", createParams.ID)
|
||||
return fmt.Errorf("user already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) updateUser(user string, rawPassword *string, isAdmin *bool) error {
|
||||
// Validate Necessary Parameters
|
||||
if rawPassword == nil && isAdmin == nil {
|
||||
return fmt.Errorf("nothing to update")
|
||||
}
|
||||
|
||||
// Base Params
|
||||
updateParams := database.UpdateUserParams{
|
||||
UserID: user,
|
||||
}
|
||||
|
||||
// Handle Admin (Update or Existing)
|
||||
if isAdmin != nil {
|
||||
updateParams.Admin = *isAdmin
|
||||
} else {
|
||||
user, err := api.db.Queries.GetUser(api.db.Ctx, user)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||
}
|
||||
updateParams.Admin = user.Admin
|
||||
}
|
||||
|
||||
// Check Admins - Disallow Demotion
|
||||
if isLast, err := api.isLastAdmin(user); err != nil {
|
||||
return err
|
||||
} else if isLast && !updateParams.Admin {
|
||||
return fmt.Errorf("unable to demote %s - last admin", user)
|
||||
}
|
||||
|
||||
// Handle Password
|
||||
if rawPassword != nil {
|
||||
if *rawPassword == "" {
|
||||
return fmt.Errorf("password can't be empty")
|
||||
}
|
||||
|
||||
// Parse Password
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword)))
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create hashed password")
|
||||
}
|
||||
updateParams.Password = &hashedPassword
|
||||
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create token for user")
|
||||
}
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
updateParams.AuthHash = &authHash
|
||||
}
|
||||
|
||||
// Update User
|
||||
_, err := api.db.Queries.UpdateUser(api.db.Ctx, updateParams)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("UpdateUser DB Error: %v", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) deleteUser(user string) error {
|
||||
// Check Admins
|
||||
if isLast, err := api.isLastAdmin(user); err != nil {
|
||||
return err
|
||||
} else if isLast {
|
||||
return fmt.Errorf("unable to delete %s - last admin", user)
|
||||
}
|
||||
|
||||
// Create Backup File
|
||||
backupFilePath := filepath.Join(api.cfg.ConfigPath, fmt.Sprintf("backups/AnthoLumeBackup_%s.zip", time.Now().Format("20060102150405")))
|
||||
backupFile, err := os.Create(backupFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer backupFile.Close()
|
||||
|
||||
// Save Backup File (DB Only)
|
||||
w := bufio.NewWriter(backupFile)
|
||||
err = api.createBackup(w, []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete User
|
||||
_, err = api.db.Queries.DeleteUser(api.db.Ctx, user)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("DeleteUser DB Error: %v", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,10 +20,12 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
"reichard.io/antholume/api/renderer"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/metadata"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
"reichard.io/antholume/ngtemplates/pages"
|
||||
"reichard.io/antholume/search"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
type backupType string
|
||||
@@ -69,7 +71,7 @@ type requestDocumentIdentify struct {
|
||||
type requestSettingsEdit struct {
|
||||
Password *string `form:"password"`
|
||||
NewPassword *string `form:"new_password"`
|
||||
TimeOffset *string `form:"time_offset"`
|
||||
Timezone *string `form:"timezone"`
|
||||
}
|
||||
|
||||
type requestDocumentAdd struct {
|
||||
@@ -101,7 +103,7 @@ func (api *API) appDocumentReader(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (api *API) appGetDocuments(c *gin.Context) {
|
||||
templateVars, auth := api.getBaseTemplateVars("documents", c)
|
||||
settings, auth := api.getBaseTemplateVarsNew(common.RouteDocuments, c)
|
||||
qParams := bindQueryParams(c, 9)
|
||||
|
||||
var query *string
|
||||
@@ -137,18 +139,15 @@ func (api *API) appGetDocuments(c *gin.Context) {
|
||||
nextPage := *qParams.Page + 1
|
||||
previousPage := *qParams.Page - 1
|
||||
|
||||
if nextPage <= totalPages {
|
||||
templateVars["NextPage"] = nextPage
|
||||
}
|
||||
|
||||
if previousPage >= 0 {
|
||||
templateVars["PreviousPage"] = previousPage
|
||||
}
|
||||
|
||||
templateVars["PageLimit"] = *qParams.Limit
|
||||
templateVars["Data"] = documents
|
||||
|
||||
c.HTML(http.StatusOK, "page/documents", templateVars)
|
||||
r := renderer.New(c.Request.Context(), http.StatusOK, pages.Documents(
|
||||
settings,
|
||||
documents,
|
||||
nextPage,
|
||||
previousPage,
|
||||
totalPages,
|
||||
*qParams.Limit,
|
||||
))
|
||||
c.Render(http.StatusOK, r)
|
||||
}
|
||||
|
||||
func (api *API) appGetDocument(c *gin.Context) {
|
||||
@@ -206,7 +205,7 @@ func (api *API) appGetProgress(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (api *API) appGetActivity(c *gin.Context) {
|
||||
templateVars, auth := api.getBaseTemplateVars("activity", c)
|
||||
settings, auth := api.getBaseTemplateVarsNew(common.RouteActivity, c)
|
||||
qParams := bindQueryParams(c, 15)
|
||||
|
||||
activityFilter := database.GetActivityParams{
|
||||
@@ -227,13 +226,15 @@ func (api *API) appGetActivity(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["Data"] = activity
|
||||
|
||||
c.HTML(http.StatusOK, "page/activity", templateVars)
|
||||
r := renderer.New(c.Request.Context(), http.StatusOK, pages.Activity(
|
||||
settings,
|
||||
activity,
|
||||
))
|
||||
c.Render(http.StatusOK, r)
|
||||
}
|
||||
|
||||
func (api *API) appGetHome(c *gin.Context) {
|
||||
templateVars, auth := api.getBaseTemplateVars("home", c)
|
||||
settings, auth := api.getBaseTemplateVarsNew(common.RouteHome, c)
|
||||
|
||||
start := time.Now()
|
||||
graphData, err := api.db.Queries.GetDailyReadStats(api.db.Ctx, auth.UserName)
|
||||
@@ -271,14 +272,19 @@ func (api *API) appGetHome(c *gin.Context) {
|
||||
}
|
||||
log.Debug("GetUserStatistics DB Performance: ", time.Since(start))
|
||||
|
||||
templateVars["Data"] = gin.H{
|
||||
"Streaks": streaks,
|
||||
"GraphData": graphData,
|
||||
"DatabaseInfo": databaseInfo,
|
||||
"UserStatistics": arrangeUserStatistics(userStatistics),
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "page/home", templateVars)
|
||||
r := renderer.New(c.Request.Context(), http.StatusOK, pages.Home(
|
||||
settings,
|
||||
getSVGGraphData(graphData, 800, 70),
|
||||
streaks,
|
||||
arrangeUserStatistics(userStatistics),
|
||||
common.UserMetadata{
|
||||
DocumentCount: int(databaseInfo.DocumentsSize),
|
||||
ActivityCount: int(databaseInfo.ActivitySize),
|
||||
ProgressCount: int(databaseInfo.ProgressSize),
|
||||
DeviceCount: int(databaseInfo.DevicesSize),
|
||||
},
|
||||
))
|
||||
c.Render(http.StatusOK, r)
|
||||
}
|
||||
|
||||
func (api *API) appGetSettings(c *gin.Context) {
|
||||
@@ -299,7 +305,7 @@ func (api *API) appGetSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
templateVars["Data"] = gin.H{
|
||||
"TimeOffset": *user.TimeOffset,
|
||||
"Timezone": *user.Timezone,
|
||||
"Devices": devices,
|
||||
}
|
||||
|
||||
@@ -314,7 +320,11 @@ func (api *API) appGetSearch(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("search", c)
|
||||
|
||||
var sParams searchParams
|
||||
c.BindQuery(&sParams)
|
||||
err := c.BindQuery(&sParams)
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Invalid Form Bind: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Only Handle Query
|
||||
if sParams.Query != nil && sParams.Source != nil {
|
||||
@@ -369,10 +379,9 @@ func (api *API) appGetDocumentProgress(c *gin.Context) {
|
||||
DocumentID: rDoc.DocumentID,
|
||||
UserID: auth.UserName,
|
||||
})
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
log.Error("UpsertDocument DB Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
log.Error("GetDocumentProgress DB Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentProgress DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -461,7 +470,8 @@ func (api *API) appUploadNewDocument(c *gin.Context) {
|
||||
|
||||
// Derive & Sanitize File Name
|
||||
fileName := deriveBaseFileName(metadataInfo)
|
||||
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
||||
basePath := filepath.Join(api.cfg.DataPath, "documents")
|
||||
safePath := filepath.Join(basePath, fileName)
|
||||
|
||||
// Open Destination File
|
||||
destFile, err := os.Create(safePath)
|
||||
@@ -488,9 +498,7 @@ func (api *API) appUploadNewDocument(c *gin.Context) {
|
||||
Md5: metadataInfo.MD5,
|
||||
Words: metadataInfo.WordCount,
|
||||
Filepath: &fileName,
|
||||
|
||||
// TODO (BasePath):
|
||||
// - Should be current config directory
|
||||
Basepath: &basePath,
|
||||
}); err != nil {
|
||||
log.Errorf("UpsertDocument DB Error: %v", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
@@ -595,7 +603,6 @@ func (api *API) appEditDocument(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "./")
|
||||
return
|
||||
}
|
||||
|
||||
func (api *API) appDeleteDocument(c *gin.Context) {
|
||||
@@ -739,52 +746,50 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Downloading document...", gin.H{"Progress": 10})
|
||||
sendDownloadMessage("Downloading document...", gin.H{"Progress": 1})
|
||||
|
||||
// Scaled Download Function
|
||||
lastTime := time.Now()
|
||||
downloadFunc := func(p float32) {
|
||||
nowTime := time.Now()
|
||||
if nowTime.Before(lastTime.Add(time.Millisecond * 500)) {
|
||||
return
|
||||
}
|
||||
scaledProgress := int((p * 95 / 100) + 2)
|
||||
sendDownloadMessage("Downloading document...", gin.H{"Progress": scaledProgress})
|
||||
lastTime = nowTime
|
||||
}
|
||||
|
||||
// Save Book
|
||||
tempFilePath, err := search.SaveBook(rDocAdd.ID, rDocAdd.Source)
|
||||
tempFilePath, metadata, err := search.SaveBook(rDocAdd.ID, rDocAdd.Source, downloadFunc)
|
||||
if err != nil {
|
||||
log.Warn("Temp File Error: ", err)
|
||||
log.Warn("Save Book Error: ", err)
|
||||
sendDownloadMessage("Unable to download file", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Calculating partial MD5...", gin.H{"Progress": 60})
|
||||
sendDownloadMessage("Saving document...", gin.H{"Progress": 98})
|
||||
|
||||
// Calculate Partial MD5 ID
|
||||
partialMD5, err := utils.CalculatePartialMD5(tempFilePath)
|
||||
if err != nil {
|
||||
log.Warn("Partial MD5 Error: ", err)
|
||||
sendDownloadMessage("Unable to calculate partial MD5", gin.H{"Error": true})
|
||||
// Derive Author / Title
|
||||
docAuthor := "Unknown"
|
||||
if *metadata.Author != "" {
|
||||
docAuthor = *metadata.Author
|
||||
} else if *rDocAdd.Author != "" {
|
||||
docAuthor = *rDocAdd.Author
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Saving file...", gin.H{"Progress": 60})
|
||||
|
||||
// Derive Extension on MIME
|
||||
fileMime, err := mimetype.DetectFile(tempFilePath)
|
||||
fileExtension := fileMime.Extension()
|
||||
|
||||
// Derive Filename
|
||||
var fileName string
|
||||
if *rDocAdd.Author != "" {
|
||||
fileName = fileName + *rDocAdd.Author
|
||||
} else {
|
||||
fileName = fileName + "Unknown"
|
||||
docTitle := "Unknown"
|
||||
if *metadata.Title != "" {
|
||||
docTitle = *metadata.Title
|
||||
} else if *rDocAdd.Title != "" {
|
||||
docTitle = *rDocAdd.Title
|
||||
}
|
||||
|
||||
if *rDocAdd.Title != "" {
|
||||
fileName = fileName + " - " + *rDocAdd.Title
|
||||
} else {
|
||||
fileName = fileName + " - Unknown"
|
||||
}
|
||||
|
||||
// Remove Slashes
|
||||
// Remove Slashes & Sanitize File Name
|
||||
fileName := fmt.Sprintf("%s - %s", docAuthor, docTitle)
|
||||
fileName = strings.ReplaceAll(fileName, "/", "")
|
||||
|
||||
// Derive & Sanitize File Name
|
||||
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *partialMD5, fileExtension))
|
||||
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadata.PartialMD5, metadata.Type))
|
||||
|
||||
// Open Source File
|
||||
sourceFile, err := os.Open(tempFilePath)
|
||||
@@ -797,7 +802,9 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
||||
defer sourceFile.Close()
|
||||
|
||||
// Generate Storage Path & Open File
|
||||
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
||||
basePath := filepath.Join(api.cfg.DataPath, "documents")
|
||||
safePath := filepath.Join(basePath, fileName)
|
||||
|
||||
destFile, err := os.Create(safePath)
|
||||
if err != nil {
|
||||
log.Error("Dest File Error: ", err)
|
||||
@@ -814,38 +821,17 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Calculating MD5...", gin.H{"Progress": 70})
|
||||
|
||||
// Get MD5 Hash
|
||||
fileHash, err := getFileMD5(safePath)
|
||||
if err != nil {
|
||||
log.Error("Hash Failure: ", err)
|
||||
sendDownloadMessage("Unable to calculate MD5", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Calculating word count...", gin.H{"Progress": 80})
|
||||
|
||||
// Get Word Count
|
||||
wordCount, err := metadata.GetWordCount(safePath)
|
||||
if err != nil {
|
||||
log.Error("Word Count Failure: ", err)
|
||||
sendDownloadMessage("Unable to calculate word count", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Saving to database...", gin.H{"Progress": 90})
|
||||
sendDownloadMessage("Saving to database...", gin.H{"Progress": 99})
|
||||
|
||||
// Upsert Document
|
||||
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||
ID: *partialMD5,
|
||||
Title: rDocAdd.Title,
|
||||
Author: rDocAdd.Author,
|
||||
Md5: fileHash,
|
||||
ID: *metadata.PartialMD5,
|
||||
Title: &docTitle,
|
||||
Author: &docAuthor,
|
||||
Md5: metadata.MD5,
|
||||
Words: metadata.WordCount,
|
||||
Filepath: &fileName,
|
||||
Words: wordCount,
|
||||
Basepath: &basePath,
|
||||
}); err != nil {
|
||||
log.Error("UpsertDocument DB Error: ", err)
|
||||
sendDownloadMessage("Unable to save to database", gin.H{"Error": true})
|
||||
@@ -856,7 +842,7 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
||||
sendDownloadMessage("Download Success", gin.H{
|
||||
"Progress": 100,
|
||||
"ButtonText": "Go to Book",
|
||||
"ButtonHref": fmt.Sprintf("./documents/%s", *partialMD5),
|
||||
"ButtonHref": fmt.Sprintf("./documents/%s", *metadata.PartialMD5),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -869,7 +855,7 @@ func (api *API) appEditSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Validate Something Exists
|
||||
if rUserSettings.Password == nil && rUserSettings.NewPassword == nil && rUserSettings.TimeOffset == nil {
|
||||
if rUserSettings.Password == nil && rUserSettings.NewPassword == nil && rUserSettings.Timezone == nil {
|
||||
log.Error("Missing Form Values")
|
||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||
return
|
||||
@@ -879,6 +865,7 @@ func (api *API) appEditSettings(c *gin.Context) {
|
||||
|
||||
newUserSettings := database.UpdateUserParams{
|
||||
UserID: auth.UserName,
|
||||
Admin: auth.IsAdmin,
|
||||
}
|
||||
|
||||
// Set New Password
|
||||
@@ -900,9 +887,9 @@ func (api *API) appEditSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Set Time Offset
|
||||
if rUserSettings.TimeOffset != nil {
|
||||
if rUserSettings.Timezone != nil {
|
||||
templateVars["TimeOffsetMessage"] = "Time Offset Updated"
|
||||
newUserSettings.TimeOffset = rUserSettings.TimeOffset
|
||||
newUserSettings.Timezone = rUserSettings.Timezone
|
||||
}
|
||||
|
||||
// Update User
|
||||
@@ -930,7 +917,7 @@ func (api *API) appEditSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
templateVars["Data"] = gin.H{
|
||||
"TimeOffset": *user.TimeOffset,
|
||||
"Timezone": *user.Timezone,
|
||||
"Devices": devices,
|
||||
}
|
||||
|
||||
@@ -950,7 +937,11 @@ func (api *API) getDocumentsWordCount(documents []database.GetDocumentsWithStats
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
for _, item := range documents {
|
||||
@@ -997,9 +988,28 @@ func (api *API) getBaseTemplateVars(routeName string, c *gin.Context) (gin.H, au
|
||||
}, auth
|
||||
}
|
||||
|
||||
func (api *API) getBaseTemplateVarsNew(route common.Route, c *gin.Context) (common.Settings, authData) {
|
||||
var auth authData
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth = data.(authData)
|
||||
}
|
||||
|
||||
return common.Settings{
|
||||
Route: route,
|
||||
User: auth.UserName,
|
||||
IsAdmin: auth.IsAdmin,
|
||||
SearchEnabled: api.cfg.SearchEnabled,
|
||||
Version: api.cfg.Version,
|
||||
}, auth
|
||||
}
|
||||
|
||||
func bindQueryParams(c *gin.Context, defaultLimit int64) queryParams {
|
||||
var qParams queryParams
|
||||
c.BindQuery(&qParams)
|
||||
err := c.BindQuery(&qParams)
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Invalid Form Bind: %v", err))
|
||||
return qParams
|
||||
}
|
||||
|
||||
if qParams.Limit == nil {
|
||||
qParams.Limit = &defaultLimit
|
||||
@@ -1037,13 +1047,13 @@ func appErrorPage(c *gin.Context, errorCode int, errorMessage string) {
|
||||
})
|
||||
}
|
||||
|
||||
func arrangeUserStatistics(userStatistics []database.GetUserStatisticsRow) gin.H {
|
||||
func arrangeUserStatistics(userStatistics []database.GetUserStatisticsRow) common.UserStatistics {
|
||||
// Item Sorter
|
||||
sortItem := func(userStatistics []database.GetUserStatisticsRow, key string, less func(i int, j int) bool) []map[string]interface{} {
|
||||
sortItem := func(userStatistics []database.GetUserStatisticsRow, key string, less func(i int, j int) bool) []common.UserStatisticEntry {
|
||||
sortedData := append([]database.GetUserStatisticsRow(nil), userStatistics...)
|
||||
sort.SliceStable(sortedData, less)
|
||||
|
||||
newData := make([]map[string]interface{}, 0)
|
||||
newData := make([]common.UserStatisticEntry, 0)
|
||||
for _, item := range sortedData {
|
||||
v := reflect.Indirect(reflect.ValueOf(item))
|
||||
|
||||
@@ -1059,17 +1069,17 @@ func arrangeUserStatistics(userStatistics []database.GetUserStatisticsRow) gin.H
|
||||
value = niceNumbers(rawVal)
|
||||
}
|
||||
|
||||
newData = append(newData, map[string]interface{}{
|
||||
"UserID": item.UserID,
|
||||
"Value": value,
|
||||
newData = append(newData, common.UserStatisticEntry{
|
||||
UserID: item.UserID,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
"WPM": gin.H{
|
||||
return common.UserStatistics{
|
||||
WPM: map[string][]common.UserStatisticEntry{
|
||||
"All": sortItem(userStatistics, "TotalWpm", func(i, j int) bool {
|
||||
return userStatistics[i].TotalWpm > userStatistics[j].TotalWpm
|
||||
}),
|
||||
@@ -1083,7 +1093,7 @@ func arrangeUserStatistics(userStatistics []database.GetUserStatisticsRow) gin.H
|
||||
return userStatistics[i].WeeklyWpm > userStatistics[j].WeeklyWpm
|
||||
}),
|
||||
},
|
||||
"Duration": gin.H{
|
||||
Duration: map[string][]common.UserStatisticEntry{
|
||||
"All": sortItem(userStatistics, "TotalSeconds", func(i, j int) bool {
|
||||
return userStatistics[i].TotalSeconds > userStatistics[j].TotalSeconds
|
||||
}),
|
||||
@@ -1097,7 +1107,7 @@ func arrangeUserStatistics(userStatistics []database.GetUserStatisticsRow) gin.H
|
||||
return userStatistics[i].WeeklySeconds > userStatistics[j].WeeklySeconds
|
||||
}),
|
||||
},
|
||||
"Words": gin.H{
|
||||
Words: map[string][]common.UserStatisticEntry{
|
||||
"All": sortItem(userStatistics, "TotalWordsRead", func(i, j int) bool {
|
||||
return userStatistics[i].TotalWordsRead > userStatistics[j].TotalWordsRead
|
||||
}),
|
||||
|
||||
72
api/auth.go
72
api/auth.go
@@ -28,22 +28,17 @@ type authKOHeader struct {
|
||||
AuthKey string `header:"x-auth-key"`
|
||||
}
|
||||
|
||||
// OPDS Auth Headers
|
||||
type authOPDSHeader struct {
|
||||
Authorization string `header:"authorization"`
|
||||
}
|
||||
|
||||
func (api *API) authorizeCredentials(username string, password string) (auth *authData) {
|
||||
user, err := api.db.Queries.GetUser(api.db.Ctx, username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || match != true {
|
||||
if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || !match {
|
||||
return
|
||||
}
|
||||
|
||||
// Update Auth Cache
|
||||
// Update auth cache
|
||||
api.userAuthCache[user.ID] = *user.AuthHash
|
||||
|
||||
return &authData{
|
||||
@@ -57,7 +52,7 @@ func (api *API) authKOMiddleware(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Check Session First
|
||||
if auth, ok := api.getSession(session); ok == true {
|
||||
if auth, ok := api.getSession(session); ok {
|
||||
c.Set("Authorization", auth)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
@@ -98,7 +93,7 @@ func (api *API) authOPDSMiddleware(c *gin.Context) {
|
||||
user, rawPassword, hasAuth := c.Request.BasicAuth()
|
||||
|
||||
// Validate Auth Fields
|
||||
if hasAuth != true || user == "" || rawPassword == "" {
|
||||
if !hasAuth || user == "" || rawPassword == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization Headers"})
|
||||
return
|
||||
}
|
||||
@@ -120,7 +115,7 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Check Session
|
||||
if auth, ok := api.getSession(session); ok == true {
|
||||
if auth, ok := api.getSession(session); ok {
|
||||
c.Set("Authorization", auth)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
@@ -129,13 +124,12 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
|
||||
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
func (api *API) authAdminWebAppMiddleware(c *gin.Context) {
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth := data.(authData)
|
||||
if auth.IsAdmin == true {
|
||||
if auth.IsAdmin {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
@@ -143,7 +137,6 @@ func (api *API) authAdminWebAppMiddleware(c *gin.Context) {
|
||||
|
||||
appErrorPage(c, http.StatusUnauthorized, "Admin Permissions Required")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
func (api *API) appAuthLogin(c *gin.Context) {
|
||||
@@ -276,7 +269,10 @@ func (api *API) appAuthRegister(c *gin.Context) {
|
||||
func (api *API) appAuthLogout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Clear()
|
||||
session.Save()
|
||||
if err := session.Save(); err != nil {
|
||||
log.Error("unable to save session")
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
}
|
||||
|
||||
@@ -377,7 +373,10 @@ func (api *API) getSession(session sessions.Session) (auth authData, ok bool) {
|
||||
// Refresh
|
||||
if expiresAt.(int64)-time.Now().Unix() < 60*60*24 {
|
||||
log.Info("Refreshing Session")
|
||||
api.setSession(session, auth)
|
||||
if err := api.setSession(session, auth); err != nil {
|
||||
log.Error("unable to get session")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Authorized
|
||||
@@ -413,30 +412,6 @@ func (api *API) getUserAuthHash(username string) (string, error) {
|
||||
return api.userAuthCache[username], nil
|
||||
}
|
||||
|
||||
func (api *API) rotateUserAuthHash(username string) error {
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
log.Error("Failed to generate user token: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update User
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
if _, err = api.db.Queries.UpdateUser(api.db.Ctx, database.UpdateUserParams{
|
||||
UserID: username,
|
||||
AuthHash: &authHash,
|
||||
}); err != nil {
|
||||
log.Error("UpdateUser DB Error: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update Cache
|
||||
api.userAuthCache[username] = fmt.Sprintf("%x", rawAuthHash)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) rotateAllAuthHashes() error {
|
||||
// Do Transaction
|
||||
tx, err := api.db.DB.Begin()
|
||||
@@ -446,7 +421,11 @@ func (api *API) rotateAllAuthHashes() error {
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
users, err := qtx.GetUsers(api.db.Ctx)
|
||||
@@ -454,7 +433,8 @@ func (api *API) rotateAllAuthHashes() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update users
|
||||
// Update Users
|
||||
newAuthHashCache := make(map[string]string, 0)
|
||||
for _, user := range users {
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
@@ -467,12 +447,13 @@ func (api *API) rotateAllAuthHashes() error {
|
||||
if _, err = qtx.UpdateUser(api.db.Ctx, database.UpdateUserParams{
|
||||
UserID: user.ID,
|
||||
AuthHash: &authHash,
|
||||
Admin: user.Admin,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update Cache
|
||||
api.userAuthCache[user.ID] = fmt.Sprintf("%x", rawAuthHash)
|
||||
// Save New Hash Cache
|
||||
newAuthHashCache[user.ID] = fmt.Sprintf("%x", rawAuthHash)
|
||||
}
|
||||
|
||||
// Commit Transaction
|
||||
@@ -481,5 +462,10 @@ func (api *API) rotateAllAuthHashes() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Transaction Succeeded -> Update Cache
|
||||
for user, hash := range newAuthHashCache {
|
||||
api.userAuthCache[user] = hash
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/metadata"
|
||||
)
|
||||
@@ -34,8 +35,14 @@ func (api *API) createDownloadDocumentHandler(errorFunc func(*gin.Context, int,
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Basepath
|
||||
basepath := filepath.Join(api.cfg.DataPath, "documents")
|
||||
if document.Basepath != nil && *document.Basepath != "" {
|
||||
basepath = *document.Basepath
|
||||
}
|
||||
|
||||
// Derive Storage Location
|
||||
filePath := filepath.Join(api.cfg.DataPath, "documents", *document.Filepath)
|
||||
filePath := filepath.Join(basepath, *document.Filepath)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(filePath)
|
||||
|
||||
@@ -193,7 +193,11 @@ func (api *API) koAddActivities(c *gin.Context) {
|
||||
allDocuments := getKeys(allDocumentsMap)
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
// Upsert Documents
|
||||
@@ -316,7 +320,11 @@ func (api *API) koAddDocuments(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
// Upsert Documents
|
||||
@@ -375,11 +383,8 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
missingDocs := []database.Document{}
|
||||
deletedDocIDs := []string{}
|
||||
|
||||
// Get Missing Documents
|
||||
missingDocs, err = api.db.Queries.GetMissingDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||
missingDocs, err := api.db.Queries.GetMissingDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("GetMissingDocuments DB Error", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
@@ -387,7 +392,7 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Get Deleted Documents
|
||||
deletedDocIDs, err = api.db.Queries.GetDeletedDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||
deletedDocIDs, err := api.db.Queries.GetDeletedDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("GetDeletedDocuments DB Error", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
@@ -494,7 +499,8 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
|
||||
})
|
||||
|
||||
// Generate Storage Path
|
||||
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
||||
basePath := filepath.Join(api.cfg.DataPath, "documents")
|
||||
safePath := filepath.Join(basePath, fileName)
|
||||
|
||||
// Save & Prevent Overwrites
|
||||
_, err = os.Stat(safePath)
|
||||
@@ -521,6 +527,7 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
|
||||
Md5: metadataInfo.MD5,
|
||||
Words: metadataInfo.WordCount,
|
||||
Filepath: &fileName,
|
||||
Basepath: &basePath,
|
||||
}); err != nil {
|
||||
log.Error("UpsertDocument DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Document Error")
|
||||
|
||||
59
api/renderer/renderer.go
Normal file
59
api/renderer/renderer.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/render"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
var Default = &HTMLTemplRenderer{}
|
||||
|
||||
type HTMLTemplRenderer struct {
|
||||
FallbackHtmlRenderer render.HTMLRender
|
||||
}
|
||||
|
||||
func (r *HTMLTemplRenderer) Instance(s string, d any) render.Render {
|
||||
templData, ok := d.(templ.Component)
|
||||
if !ok {
|
||||
if r.FallbackHtmlRenderer != nil {
|
||||
return r.FallbackHtmlRenderer.Instance(s, d)
|
||||
}
|
||||
}
|
||||
return &Renderer{
|
||||
Ctx: context.Background(),
|
||||
Status: -1,
|
||||
Component: templData,
|
||||
}
|
||||
}
|
||||
|
||||
func New(ctx context.Context, status int, component templ.Component) *Renderer {
|
||||
return &Renderer{
|
||||
Ctx: ctx,
|
||||
Status: status,
|
||||
Component: component,
|
||||
}
|
||||
}
|
||||
|
||||
type Renderer struct {
|
||||
Ctx context.Context
|
||||
Status int
|
||||
Component templ.Component
|
||||
}
|
||||
|
||||
func (t Renderer) Render(w http.ResponseWriter) error {
|
||||
t.WriteContentType(w)
|
||||
if t.Status != -1 {
|
||||
w.WriteHeader(t.Status)
|
||||
}
|
||||
if t.Component != nil {
|
||||
return t.Component.Render(t.Ctx, w)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t Renderer) WriteContentType(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
}
|
||||
131
api/utils.go
131
api/utils.go
@@ -13,56 +13,48 @@ import (
|
||||
"reichard.io/antholume/metadata"
|
||||
)
|
||||
|
||||
type UTCOffset struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
var UTC_OFFSETS = []UTCOffset{
|
||||
{Value: "-12 hours", Name: "UTC−12:00"},
|
||||
{Value: "-11 hours", Name: "UTC−11:00"},
|
||||
{Value: "-10 hours", Name: "UTC−10:00"},
|
||||
{Value: "-9.5 hours", Name: "UTC−09:30"},
|
||||
{Value: "-9 hours", Name: "UTC−09:00"},
|
||||
{Value: "-8 hours", Name: "UTC−08:00"},
|
||||
{Value: "-7 hours", Name: "UTC−07:00"},
|
||||
{Value: "-6 hours", Name: "UTC−06:00"},
|
||||
{Value: "-5 hours", Name: "UTC−05:00"},
|
||||
{Value: "-4 hours", Name: "UTC−04:00"},
|
||||
{Value: "-3.5 hours", Name: "UTC−03:30"},
|
||||
{Value: "-3 hours", Name: "UTC−03:00"},
|
||||
{Value: "-2 hours", Name: "UTC−02:00"},
|
||||
{Value: "-1 hours", Name: "UTC−01:00"},
|
||||
{Value: "0 hours", Name: "UTC±00:00"},
|
||||
{Value: "+1 hours", Name: "UTC+01:00"},
|
||||
{Value: "+2 hours", Name: "UTC+02:00"},
|
||||
{Value: "+3 hours", Name: "UTC+03:00"},
|
||||
{Value: "+3.5 hours", Name: "UTC+03:30"},
|
||||
{Value: "+4 hours", Name: "UTC+04:00"},
|
||||
{Value: "+4.5 hours", Name: "UTC+04:30"},
|
||||
{Value: "+5 hours", Name: "UTC+05:00"},
|
||||
{Value: "+5.5 hours", Name: "UTC+05:30"},
|
||||
{Value: "+5.75 hours", Name: "UTC+05:45"},
|
||||
{Value: "+6 hours", Name: "UTC+06:00"},
|
||||
{Value: "+6.5 hours", Name: "UTC+06:30"},
|
||||
{Value: "+7 hours", Name: "UTC+07:00"},
|
||||
{Value: "+8 hours", Name: "UTC+08:00"},
|
||||
{Value: "+8.75 hours", Name: "UTC+08:45"},
|
||||
{Value: "+9 hours", Name: "UTC+09:00"},
|
||||
{Value: "+9.5 hours", Name: "UTC+09:30"},
|
||||
{Value: "+10 hours", Name: "UTC+10:00"},
|
||||
{Value: "+10.5 hours", Name: "UTC+10:30"},
|
||||
{Value: "+11 hours", Name: "UTC+11:00"},
|
||||
{Value: "+12 hours", Name: "UTC+12:00"},
|
||||
{Value: "+12.75 hours", Name: "UTC+12:45"},
|
||||
{Value: "+13 hours", Name: "UTC+13:00"},
|
||||
{Value: "+14 hours", Name: "UTC+14:00"},
|
||||
}
|
||||
|
||||
func getUTCOffsets() []UTCOffset {
|
||||
return UTC_OFFSETS
|
||||
// getTimeZones returns a string slice of IANA timezones.
|
||||
func getTimeZones() []string {
|
||||
return []string{
|
||||
"Africa/Cairo",
|
||||
"Africa/Johannesburg",
|
||||
"Africa/Lagos",
|
||||
"Africa/Nairobi",
|
||||
"America/Adak",
|
||||
"America/Anchorage",
|
||||
"America/Buenos_Aires",
|
||||
"America/Chicago",
|
||||
"America/Denver",
|
||||
"America/Los_Angeles",
|
||||
"America/Mexico_City",
|
||||
"America/New_York",
|
||||
"America/Nuuk",
|
||||
"America/Phoenix",
|
||||
"America/Puerto_Rico",
|
||||
"America/Sao_Paulo",
|
||||
"America/St_Johns",
|
||||
"America/Toronto",
|
||||
"Asia/Dubai",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Kolkata",
|
||||
"Asia/Seoul",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Singapore",
|
||||
"Asia/Tokyo",
|
||||
"Atlantic/Azores",
|
||||
"Australia/Melbourne",
|
||||
"Australia/Sydney",
|
||||
"Europe/Berlin",
|
||||
"Europe/London",
|
||||
"Europe/Moscow",
|
||||
"Europe/Paris",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Honolulu",
|
||||
}
|
||||
}
|
||||
|
||||
// niceSeconds takes in an int (in seconds) and returns a string readable
|
||||
// representation. For example 1928371 -> "22d 7h 39m 31s".
|
||||
func niceSeconds(input int64) (result string) {
|
||||
if input == 0 {
|
||||
return "N/A"
|
||||
@@ -91,6 +83,8 @@ func niceSeconds(input int64) (result string) {
|
||||
return
|
||||
}
|
||||
|
||||
// niceNumbers takes in an int and returns a string representation. For example
|
||||
// 19823 -> "19.8k".
|
||||
func niceNumbers(input int64) string {
|
||||
if input == 0 {
|
||||
return "0"
|
||||
@@ -109,21 +103,27 @@ func niceNumbers(input int64) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Database Array -> Int64 Array
|
||||
// getSVGGraphData builds SVGGraphData from the provided stats, width and height.
|
||||
// It is used exclusively in templates to generate the daily read stats graph.
|
||||
func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, svgHeight int) graph.SVGGraphData {
|
||||
var intData []int64
|
||||
var intData []graph.SVGRawData
|
||||
for _, item := range inputData {
|
||||
intData = append(intData, item.MinutesRead)
|
||||
intData = append(intData, graph.SVGRawData{
|
||||
Value: int(item.MinutesRead),
|
||||
Label: item.Date,
|
||||
})
|
||||
}
|
||||
|
||||
return graph.GetSVGGraphData(intData, svgWidth, svgHeight)
|
||||
}
|
||||
|
||||
func dict(values ...interface{}) (map[string]interface{}, error) {
|
||||
// dict returns a map[string]any dict. Each pair of two is a key & value
|
||||
// respectively. It's primarily utilized in templates.
|
||||
func dict(values ...any) (map[string]any, error) {
|
||||
if len(values)%2 != 0 {
|
||||
return nil, errors.New("invalid dict call")
|
||||
}
|
||||
dict := make(map[string]interface{}, len(values)/2)
|
||||
dict := make(map[string]any, len(values)/2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
@@ -134,12 +134,14 @@ func dict(values ...interface{}) (map[string]interface{}, error) {
|
||||
return dict, nil
|
||||
}
|
||||
|
||||
func fields(value interface{}) (map[string]interface{}, error) {
|
||||
// fields returns a map[string]any of the provided struct. It's primarily
|
||||
// utilized in templates.
|
||||
func fields(value any) (map[string]any, error) {
|
||||
v := reflect.Indirect(reflect.ValueOf(value))
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("%T is not a struct", value)
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
m := make(map[string]any)
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
sv := t.Field(i)
|
||||
@@ -148,6 +150,13 @@ func fields(value interface{}) (map[string]interface{}, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// slice returns a slice of the provided arguments. It's primarily utilized in
|
||||
// templates.
|
||||
func slice(elements ...any) []any {
|
||||
return elements
|
||||
}
|
||||
|
||||
// deriveBaseFileName builds the base filename for a given MetadataInfo object.
|
||||
func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
|
||||
// Derive New FileName
|
||||
var newFileName string
|
||||
@@ -166,3 +175,15 @@ func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
|
||||
fileName := strings.ReplaceAll(newFileName, "/", "")
|
||||
return "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadataInfo.PartialMD5, metadataInfo.Type))
|
||||
}
|
||||
|
||||
// importStatusPriority returns the order priority for import status in the UI.
|
||||
func importStatusPriority(status importStatus) int {
|
||||
switch status {
|
||||
case importFailed:
|
||||
return 1
|
||||
case importExists:
|
||||
return 2
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
20
assets/sw.js
20
assets/sw.js
@@ -99,7 +99,7 @@ const PRECACHE_ASSETS = [
|
||||
// ----------------------- Helpers ----------------------- //
|
||||
// ------------------------------------------------------- //
|
||||
|
||||
function purgeCache() {
|
||||
async function purgeCache() {
|
||||
console.log("[purgeCache] Purging Cache");
|
||||
return caches.keys().then(function (names) {
|
||||
for (let name of names) caches.delete(name);
|
||||
@@ -136,7 +136,7 @@ async function handleFetch(event) {
|
||||
const directive = ROUTES.find(
|
||||
(item) =>
|
||||
(item.route instanceof RegExp && url.match(item.route)) ||
|
||||
url == item.route
|
||||
url == item.route,
|
||||
) || { type: CACHE_NEVER };
|
||||
|
||||
// Get Fallback
|
||||
@@ -161,11 +161,11 @@ async function handleFetch(event) {
|
||||
);
|
||||
case CACHE_UPDATE_SYNC:
|
||||
return updateCache(event.request).catch(
|
||||
(e) => currentCache || fallbackFunc(event)
|
||||
(e) => currentCache || fallbackFunc(event),
|
||||
);
|
||||
case CACHE_UPDATE_ASYNC:
|
||||
let newResponse = updateCache(event.request).catch((e) =>
|
||||
fallbackFunc(event)
|
||||
fallbackFunc(event),
|
||||
);
|
||||
|
||||
return currentCache || newResponse;
|
||||
@@ -192,7 +192,7 @@ function handleMessage(event) {
|
||||
.filter(
|
||||
(item) =>
|
||||
item.startsWith("/documents/") ||
|
||||
item.startsWith("/reader/progress/")
|
||||
item.startsWith("/reader/progress/"),
|
||||
);
|
||||
|
||||
// Derive Unique IDs
|
||||
@@ -200,8 +200,8 @@ function handleMessage(event) {
|
||||
new Set(
|
||||
docResources
|
||||
.filter((item) => item.startsWith("/documents/"))
|
||||
.map((item) => item.split("/")[2])
|
||||
)
|
||||
.map((item) => item.split("/")[2]),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -214,14 +214,14 @@ function handleMessage(event) {
|
||||
.filter(
|
||||
(id) =>
|
||||
docResources.includes("/documents/" + id + "/file") &&
|
||||
docResources.includes("/reader/progress/" + id)
|
||||
docResources.includes("/reader/progress/" + id),
|
||||
)
|
||||
.map(async (id) => {
|
||||
let url = "/reader/progress/" + id;
|
||||
let currentCache = await caches.match(url);
|
||||
let resp = await updateCache(url).catch((e) => currentCache);
|
||||
return resp.json();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
event.source.postMessage({ id, data: cachedDocuments });
|
||||
@@ -233,7 +233,7 @@ function handleMessage(event) {
|
||||
Promise.all([
|
||||
cache.delete("/documents/" + data.id + "/file"),
|
||||
cache.delete("/reader/progress/" + data.id),
|
||||
])
|
||||
]),
|
||||
)
|
||||
.then(() => event.source.postMessage({ id, data: "SUCCESS" }))
|
||||
.catch(() => event.source.postMessage({ id, data: "FAILURE" }));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.27.0
|
||||
|
||||
package database
|
||||
|
||||
|
||||
151
database/document_user_statistics.sql
Normal file
151
database/document_user_statistics.sql
Normal file
@@ -0,0 +1,151 @@
|
||||
WITH grouped_activity AS (
|
||||
SELECT
|
||||
ga.user_id,
|
||||
ga.document_id,
|
||||
MAX(ga.created_at) AS created_at,
|
||||
MAX(ga.start_time) AS start_time,
|
||||
MIN(ga.start_percentage) AS start_percentage,
|
||||
MAX(ga.end_percentage) AS end_percentage,
|
||||
|
||||
-- Total Duration & Percentage
|
||||
SUM(ga.duration) AS total_time_seconds,
|
||||
SUM(ga.end_percentage - ga.start_percentage) AS total_read_percentage,
|
||||
|
||||
-- Yearly Duration
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-1 year')
|
||||
THEN ga.duration
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS yearly_time_seconds,
|
||||
|
||||
-- Yearly Percentage
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-1 year')
|
||||
THEN ga.end_percentage - ga.start_percentage
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS yearly_read_percentage,
|
||||
|
||||
-- Monthly Duration
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-1 month')
|
||||
THEN ga.duration
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS monthly_time_seconds,
|
||||
|
||||
-- Monthly Percentage
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-1 month')
|
||||
THEN ga.end_percentage - ga.start_percentage
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS monthly_read_percentage,
|
||||
|
||||
-- Weekly Duration
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-7 days')
|
||||
THEN ga.duration
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS weekly_time_seconds,
|
||||
|
||||
-- Weekly Percentage
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-7 days')
|
||||
THEN ga.end_percentage - ga.start_percentage
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS weekly_read_percentage
|
||||
|
||||
FROM activity AS ga
|
||||
GROUP BY ga.user_id, ga.document_id
|
||||
),
|
||||
|
||||
current_progress AS (
|
||||
SELECT
|
||||
user_id,
|
||||
document_id,
|
||||
COALESCE((
|
||||
SELECT dp.percentage
|
||||
FROM document_progress AS dp
|
||||
WHERE
|
||||
dp.user_id = iga.user_id
|
||||
AND dp.document_id = iga.document_id
|
||||
ORDER BY dp.created_at DESC
|
||||
LIMIT 1
|
||||
), end_percentage) AS percentage
|
||||
FROM grouped_activity AS iga
|
||||
)
|
||||
|
||||
INSERT INTO document_user_statistics
|
||||
SELECT
|
||||
ga.document_id,
|
||||
ga.user_id,
|
||||
cp.percentage,
|
||||
MAX(ga.start_time) AS last_read,
|
||||
MAX(ga.created_at) AS last_seen,
|
||||
SUM(ga.total_read_percentage) AS read_percentage,
|
||||
|
||||
-- All Time WPM
|
||||
SUM(ga.total_time_seconds) AS total_time_seconds,
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(ga.total_read_percentage))
|
||||
AS total_words_read,
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(ga.total_read_percentage))
|
||||
/ (SUM(ga.total_time_seconds) / 60.0) AS total_wpm,
|
||||
|
||||
-- Yearly WPM
|
||||
ga.yearly_time_seconds,
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL) * ga.yearly_read_percentage
|
||||
AS yearly_words_read,
|
||||
COALESCE(
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * ga.yearly_read_percentage)
|
||||
/ (ga.yearly_time_seconds / 60), 0.0)
|
||||
AS yearly_wpm,
|
||||
|
||||
-- Monthly WPM
|
||||
ga.monthly_time_seconds,
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL) * ga.monthly_read_percentage
|
||||
AS monthly_words_read,
|
||||
COALESCE(
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * ga.monthly_read_percentage)
|
||||
/ (ga.monthly_time_seconds / 60), 0.0)
|
||||
AS monthly_wpm,
|
||||
|
||||
-- Weekly WPM
|
||||
ga.weekly_time_seconds,
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL) * ga.weekly_read_percentage
|
||||
AS weekly_words_read,
|
||||
COALESCE(
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * ga.weekly_read_percentage)
|
||||
/ (ga.weekly_time_seconds / 60), 0.0)
|
||||
AS weekly_wpm
|
||||
|
||||
FROM grouped_activity AS ga
|
||||
INNER JOIN
|
||||
current_progress AS cp
|
||||
ON ga.user_id = cp.user_id AND ga.document_id = cp.document_id
|
||||
INNER JOIN
|
||||
documents AS d
|
||||
ON ga.document_id = d.id
|
||||
GROUP BY ga.document_id, ga.user_id
|
||||
ORDER BY total_wpm DESC;
|
||||
114
database/documents_test.go
Normal file
114
database/documents_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"reichard.io/antholume/config"
|
||||
)
|
||||
|
||||
type DocumentsTestSuite struct {
|
||||
suite.Suite
|
||||
dbm *DBManager
|
||||
}
|
||||
|
||||
func TestDocuments(t *testing.T) {
|
||||
suite.Run(t, new(DocumentsTestSuite))
|
||||
}
|
||||
|
||||
func (suite *DocumentsTestSuite) SetupTest() {
|
||||
cfg := config.Config{
|
||||
DBType: "memory",
|
||||
}
|
||||
|
||||
suite.dbm = NewMgr(&cfg)
|
||||
|
||||
// Create Document
|
||||
_, err := suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
|
||||
ID: documentID,
|
||||
Title: &documentTitle,
|
||||
Author: &documentAuthor,
|
||||
Words: &documentWords,
|
||||
})
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
// DOCUMENT - TODO:
|
||||
// - (q *Queries) GetDocumentProgress
|
||||
// - (q *Queries) GetDocumentWithStats
|
||||
// - (q *Queries) GetDocumentsSize
|
||||
// - (q *Queries) GetDocumentsWithStats
|
||||
// - (q *Queries) GetMissingDocuments
|
||||
func (suite *DocumentsTestSuite) TestGetDocument() {
|
||||
doc, err := suite.dbm.Queries.GetDocument(suite.dbm.Ctx, documentID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(documentID, doc.ID, "should have changed the document")
|
||||
}
|
||||
|
||||
func (suite *DocumentsTestSuite) TestUpsertDocument() {
|
||||
testDocID := "docid1"
|
||||
|
||||
doc, err := suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
|
||||
ID: testDocID,
|
||||
Title: &documentTitle,
|
||||
Author: &documentAuthor,
|
||||
})
|
||||
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(testDocID, doc.ID, "should have document id")
|
||||
suite.Equal(documentTitle, *doc.Title, "should have document title")
|
||||
suite.Equal(documentAuthor, *doc.Author, "should have document author")
|
||||
}
|
||||
|
||||
func (suite *DocumentsTestSuite) TestDeleteDocument() {
|
||||
changed, err := suite.dbm.Queries.DeleteDocument(suite.dbm.Ctx, documentID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(int64(1), changed, "should have changed the document")
|
||||
|
||||
doc, err := suite.dbm.Queries.GetDocument(suite.dbm.Ctx, documentID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.True(doc.Deleted, "should have deleted the document")
|
||||
}
|
||||
|
||||
func (suite *DocumentsTestSuite) TestGetDeletedDocuments() {
|
||||
changed, err := suite.dbm.Queries.DeleteDocument(suite.dbm.Ctx, documentID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(int64(1), changed, "should have changed the document")
|
||||
|
||||
deletedDocs, err := suite.dbm.Queries.GetDeletedDocuments(suite.dbm.Ctx, []string{documentID})
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(deletedDocs, 1, "should have one deleted document")
|
||||
}
|
||||
|
||||
// TODO - Convert GetWantedDocuments -> (sqlc.slice('document_ids'));
|
||||
func (suite *DocumentsTestSuite) TestGetWantedDocuments() {
|
||||
wantedDocs, err := suite.dbm.Queries.GetWantedDocuments(suite.dbm.Ctx, fmt.Sprintf("[\"%s\"]", documentID))
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(wantedDocs, 1, "should have one wanted document")
|
||||
}
|
||||
|
||||
func (suite *DocumentsTestSuite) TestGetMissingDocuments() {
|
||||
// Create Document
|
||||
_, err := suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
|
||||
ID: documentID,
|
||||
Filepath: &documentFilepath,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
missingDocs, err := suite.dbm.Queries.GetMissingDocuments(suite.dbm.Ctx, []string{documentID})
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(missingDocs, 0, "should have no wanted document")
|
||||
|
||||
missingDocs, err = suite.dbm.Queries.GetMissingDocuments(suite.dbm.Ctx, []string{"other"})
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(missingDocs, 1, "should have one missing document")
|
||||
suite.Equal(documentID, missingDocs[0].ID, "should have missing doc")
|
||||
|
||||
// TODO - https://github.com/sqlc-dev/sqlc/issues/3451
|
||||
// missingDocs, err = suite.dbm.Queries.GetMissingDocuments(suite.dbm.Ctx, []string{})
|
||||
// suite.Nil(err, "should have nil err")
|
||||
// suite.Len(missingDocs, 1, "should have one missing document")
|
||||
// suite.Equal(documentID, missingDocs[0].ID, "should have missing doc")
|
||||
}
|
||||
@@ -3,15 +3,17 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"embed"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "modernc.org/sqlite"
|
||||
sqlite "modernc.org/sqlite"
|
||||
"reichard.io/antholume/config"
|
||||
_ "reichard.io/antholume/database/migrations"
|
||||
)
|
||||
@@ -26,10 +28,30 @@ type DBManager struct {
|
||||
//go:embed schema.sql
|
||||
var ddl string
|
||||
|
||||
//go:embed user_streaks.sql
|
||||
var user_streaks string
|
||||
|
||||
//go:embed document_user_statistics.sql
|
||||
var document_user_statistics string
|
||||
|
||||
//go:embed migrations/*
|
||||
var migrations embed.FS
|
||||
|
||||
// Returns an initialized manager
|
||||
// Register scalar sqlite function on init
|
||||
func init() {
|
||||
sqlite.MustRegisterFunction("LOCAL_TIME", &sqlite.FunctionImpl{
|
||||
NArgs: 2,
|
||||
Deterministic: true,
|
||||
Scalar: localTime,
|
||||
})
|
||||
sqlite.MustRegisterFunction("LOCAL_DATE", &sqlite.FunctionImpl{
|
||||
NArgs: 2,
|
||||
Deterministic: true,
|
||||
Scalar: localDate,
|
||||
})
|
||||
}
|
||||
|
||||
// NewMgr Returns an initialized manager
|
||||
func NewMgr(c *config.Config) *DBManager {
|
||||
// Create Manager
|
||||
dbm := &DBManager{
|
||||
@@ -44,7 +66,7 @@ func NewMgr(c *config.Config) *DBManager {
|
||||
return dbm
|
||||
}
|
||||
|
||||
// Init manager
|
||||
// init loads the DB manager
|
||||
func (dbm *DBManager) init() error {
|
||||
// Build DB Location
|
||||
var dbLocation string
|
||||
@@ -98,12 +120,14 @@ func (dbm *DBManager) init() error {
|
||||
}
|
||||
|
||||
// Cache tables
|
||||
go dbm.CacheTempTables()
|
||||
if err := dbm.CacheTempTables(); err != nil {
|
||||
log.Warn("Refreshing temp table cache failed: ", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload manager (close DB & reinit)
|
||||
// Reload closes the DB & reinits
|
||||
func (dbm *DBManager) Reload() error {
|
||||
// Close handle
|
||||
err := dbm.DB.Close()
|
||||
@@ -119,23 +143,16 @@ func (dbm *DBManager) Reload() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheTempTables clears existing statistics and recalculates
|
||||
func (dbm *DBManager) CacheTempTables() error {
|
||||
start := time.Now()
|
||||
user_streaks_sql := `
|
||||
DELETE FROM user_streaks;
|
||||
INSERT INTO user_streaks SELECT * FROM view_user_streaks;
|
||||
`
|
||||
if _, err := dbm.DB.ExecContext(dbm.Ctx, user_streaks_sql); err != nil {
|
||||
if _, err := dbm.DB.ExecContext(dbm.Ctx, user_streaks); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("Cached 'user_streaks' in: ", time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
document_statistics_sql := `
|
||||
DELETE FROM document_user_statistics;
|
||||
INSERT INTO document_user_statistics SELECT * FROM view_document_user_statistics;
|
||||
`
|
||||
if _, err := dbm.DB.ExecContext(dbm.Ctx, document_statistics_sql); err != nil {
|
||||
if _, err := dbm.DB.ExecContext(dbm.Ctx, document_user_statistics); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("Cached 'document_user_statistics' in: ", time.Since(start))
|
||||
@@ -143,6 +160,8 @@ func (dbm *DBManager) CacheTempTables() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateSettings ensures that we're enforcing foreign keys and enable journal
|
||||
// mode.
|
||||
func (dbm *DBManager) updateSettings() error {
|
||||
// Set SQLite PRAGMA Settings
|
||||
pragmaQuery := `
|
||||
@@ -166,9 +185,10 @@ func (dbm *DBManager) updateSettings() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// performMigrations runs all migrations
|
||||
func (dbm *DBManager) performMigrations(isNew bool) error {
|
||||
// Create context
|
||||
ctx := context.WithValue(context.Background(), "isNew", isNew)
|
||||
ctx := context.WithValue(context.Background(), "isNew", isNew) // nolint
|
||||
|
||||
// Set DB migration
|
||||
goose.SetBaseFS(migrations)
|
||||
@@ -182,6 +202,7 @@ func (dbm *DBManager) performMigrations(isNew bool) error {
|
||||
return goose.UpContext(ctx, dbm.DB, "migrations")
|
||||
}
|
||||
|
||||
// isEmpty determines whether the database is empty
|
||||
func isEmpty(db *sql.DB) (bool, error) {
|
||||
var tableCount int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table';").Scan(&tableCount)
|
||||
@@ -190,3 +211,53 @@ func isEmpty(db *sql.DB) (bool, error) {
|
||||
}
|
||||
return tableCount == 0, nil
|
||||
}
|
||||
|
||||
// localTime is a custom SQL function that is registered as LOCAL_TIME in the init function
|
||||
func localTime(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
|
||||
timeStr, ok := args[0].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("both arguments to TZTime must be strings")
|
||||
}
|
||||
|
||||
timeZoneStr, ok := args[1].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("both arguments to TZTime must be strings")
|
||||
}
|
||||
|
||||
timeZone, err := time.LoadLocation(timeZoneStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to parse timezone")
|
||||
}
|
||||
|
||||
formattedTime, err := time.ParseInLocation(time.RFC3339, timeStr, time.UTC)
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to parse time")
|
||||
}
|
||||
|
||||
return formattedTime.In(timeZone).Format(time.RFC3339), nil
|
||||
}
|
||||
|
||||
// localDate is a custom SQL function that is registered as LOCAL_DATE in the init function
|
||||
func localDate(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
|
||||
timeStr, ok := args[0].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("both arguments to TZTime must be strings")
|
||||
}
|
||||
|
||||
timeZoneStr, ok := args[1].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("both arguments to TZTime must be strings")
|
||||
}
|
||||
|
||||
timeZone, err := time.LoadLocation(timeZoneStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to parse timezone")
|
||||
}
|
||||
|
||||
formattedTime, err := time.ParseInLocation(time.RFC3339, timeStr, time.UTC)
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to parse time")
|
||||
}
|
||||
|
||||
return formattedTime.In(timeZone).Format("2006-01-02"), nil
|
||||
}
|
||||
|
||||
@@ -5,98 +5,73 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"reichard.io/antholume/config"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
type databaseTest struct {
|
||||
*testing.T
|
||||
var (
|
||||
userID string = "testUser"
|
||||
userPass string = "testPass"
|
||||
deviceID string = "testDevice"
|
||||
deviceName string = "testDeviceName"
|
||||
documentID string = "testDocument"
|
||||
documentTitle string = "testTitle"
|
||||
documentAuthor string = "testAuthor"
|
||||
documentFilepath string = "./testPath.epub"
|
||||
documentWords int64 = 5000
|
||||
)
|
||||
|
||||
type DatabaseTestSuite struct {
|
||||
suite.Suite
|
||||
dbm *DBManager
|
||||
}
|
||||
|
||||
var userID string = "testUser"
|
||||
var userPass string = "testPass"
|
||||
var deviceID string = "testDevice"
|
||||
var deviceName string = "testDeviceName"
|
||||
var documentID string = "testDocument"
|
||||
var documentTitle string = "testTitle"
|
||||
var documentAuthor string = "testAuthor"
|
||||
func TestDatabase(t *testing.T) {
|
||||
suite.Run(t, new(DatabaseTestSuite))
|
||||
}
|
||||
|
||||
func TestNewMgr(t *testing.T) {
|
||||
// PROGRESS - TODO:
|
||||
// - (q *Queries) GetProgress
|
||||
// - (q *Queries) UpdateProgress
|
||||
|
||||
func (suite *DatabaseTestSuite) SetupTest() {
|
||||
cfg := config.Config{
|
||||
DBType: "memory",
|
||||
}
|
||||
|
||||
dbm := NewMgr(&cfg)
|
||||
assert.NotNil(t, dbm, "should not have nil dbm")
|
||||
|
||||
t.Run("Database", func(t *testing.T) {
|
||||
dt := databaseTest{t, dbm}
|
||||
dt.TestUser()
|
||||
dt.TestDocument()
|
||||
dt.TestDevice()
|
||||
dt.TestActivity()
|
||||
dt.TestDailyReadStats()
|
||||
})
|
||||
}
|
||||
|
||||
func (dt *databaseTest) TestUser() {
|
||||
dt.Run("User", func(t *testing.T) {
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
assert.Nil(t, err, "should have nil err")
|
||||
suite.dbm = NewMgr(&cfg)
|
||||
|
||||
// Create User
|
||||
rawAuthHash, _ := utils.GenerateToken(64)
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
changed, err := dt.dbm.Queries.CreateUser(dt.dbm.Ctx, CreateUserParams{
|
||||
_, err := suite.dbm.Queries.CreateUser(suite.dbm.Ctx, CreateUserParams{
|
||||
ID: userID,
|
||||
Pass: &userPass,
|
||||
AuthHash: &authHash,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
assert.Nil(t, err, "should have nil err")
|
||||
assert.Equal(t, int64(1), changed)
|
||||
|
||||
user, err := dt.dbm.Queries.GetUser(dt.dbm.Ctx, userID)
|
||||
|
||||
assert.Nil(t, err, "should have nil err")
|
||||
assert.Equal(t, userPass, *user.Pass)
|
||||
})
|
||||
}
|
||||
|
||||
func (dt *databaseTest) TestDocument() {
|
||||
dt.Run("Document", func(t *testing.T) {
|
||||
doc, err := dt.dbm.Queries.UpsertDocument(dt.dbm.Ctx, UpsertDocumentParams{
|
||||
// Create Document
|
||||
_, err = suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
|
||||
ID: documentID,
|
||||
Title: &documentTitle,
|
||||
Author: &documentAuthor,
|
||||
Filepath: &documentFilepath,
|
||||
Words: &documentWords,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
assert.Nil(t, err, "should have nil err")
|
||||
assert.Equal(t, documentID, doc.ID, "should have document id")
|
||||
assert.Equal(t, documentTitle, *doc.Title, "should have document title")
|
||||
assert.Equal(t, documentAuthor, *doc.Author, "should have document author")
|
||||
})
|
||||
}
|
||||
|
||||
func (dt *databaseTest) TestDevice() {
|
||||
dt.Run("Device", func(t *testing.T) {
|
||||
device, err := dt.dbm.Queries.UpsertDevice(dt.dbm.Ctx, UpsertDeviceParams{
|
||||
// Create Device
|
||||
_, err = suite.dbm.Queries.UpsertDevice(suite.dbm.Ctx, UpsertDeviceParams{
|
||||
ID: deviceID,
|
||||
UserID: userID,
|
||||
DeviceName: deviceName,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
assert.Nil(t, err, "should have nil err")
|
||||
assert.Equal(t, deviceID, device.ID, "should have device id")
|
||||
assert.Equal(t, userID, device.UserID, "should have user id")
|
||||
assert.Equal(t, deviceName, device.DeviceName, "should have device name")
|
||||
})
|
||||
}
|
||||
|
||||
func (dt *databaseTest) TestActivity() {
|
||||
dt.Run("Progress", func(t *testing.T) {
|
||||
// 10 Activities, 10 Days
|
||||
// Create Activity
|
||||
end := time.Now()
|
||||
start := end.AddDate(0, 0, -9)
|
||||
var counter int64 = 0
|
||||
@@ -105,7 +80,7 @@ func (dt *databaseTest) TestActivity() {
|
||||
counter += 1
|
||||
|
||||
// Add Item
|
||||
activity, err := dt.dbm.Queries.AddActivity(dt.dbm.Ctx, AddActivityParams{
|
||||
activity, err := suite.dbm.Queries.AddActivity(suite.dbm.Ctx, AddActivityParams{
|
||||
DocumentID: documentID,
|
||||
DeviceID: deviceID,
|
||||
UserID: userID,
|
||||
@@ -115,25 +90,50 @@ func (dt *databaseTest) TestActivity() {
|
||||
EndPercentage: float64(counter+1) / 100.0,
|
||||
})
|
||||
|
||||
assert.Nil(t, err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
|
||||
assert.Equal(t, counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
|
||||
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
|
||||
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
|
||||
}
|
||||
|
||||
// Initiate Cache
|
||||
dt.dbm.CacheTempTables()
|
||||
err = suite.dbm.CacheTempTables()
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
// DEVICES - TODO:
|
||||
// - (q *Queries) GetDevice
|
||||
// - (q *Queries) GetDevices
|
||||
// - (q *Queries) UpsertDevice
|
||||
func (suite *DatabaseTestSuite) TestDevice() {
|
||||
testDevice := "dev123"
|
||||
device, err := suite.dbm.Queries.UpsertDevice(suite.dbm.Ctx, UpsertDeviceParams{
|
||||
ID: testDevice,
|
||||
UserID: userID,
|
||||
DeviceName: deviceName,
|
||||
})
|
||||
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(testDevice, device.ID, "should have device id")
|
||||
suite.Equal(userID, device.UserID, "should have user id")
|
||||
suite.Equal(deviceName, device.DeviceName, "should have device name")
|
||||
}
|
||||
|
||||
// ACTIVITY - TODO:
|
||||
// - (q *Queries) AddActivity
|
||||
// - (q *Queries) GetActivity
|
||||
// - (q *Queries) GetLastActivity
|
||||
func (suite *DatabaseTestSuite) TestActivity() {
|
||||
// Validate Exists
|
||||
existsRows, err := dt.dbm.Queries.GetActivity(dt.dbm.Ctx, GetActivityParams{
|
||||
existsRows, err := suite.dbm.Queries.GetActivity(suite.dbm.Ctx, GetActivityParams{
|
||||
UserID: userID,
|
||||
Offset: 0,
|
||||
Limit: 50,
|
||||
})
|
||||
|
||||
assert.Nil(t, err, "should have nil err for get activity")
|
||||
assert.Len(t, existsRows, 10, "should have correct number of rows get activity")
|
||||
suite.Nil(err, "should have nil err for get activity")
|
||||
suite.Len(existsRows, 10, "should have correct number of rows get activity")
|
||||
|
||||
// Validate Doesn't Exist
|
||||
doesntExistsRows, err := dt.dbm.Queries.GetActivity(dt.dbm.Ctx, GetActivityParams{
|
||||
doesntExistsRows, err := suite.dbm.Queries.GetActivity(suite.dbm.Ctx, GetActivityParams{
|
||||
UserID: userID,
|
||||
DocumentID: "unknownDoc",
|
||||
DocFilter: true,
|
||||
@@ -141,28 +141,30 @@ func (dt *databaseTest) TestActivity() {
|
||||
Limit: 50,
|
||||
})
|
||||
|
||||
assert.Nil(t, err, "should have nil err for get activity")
|
||||
assert.Len(t, doesntExistsRows, 0, "should have no rows")
|
||||
})
|
||||
suite.Nil(err, "should have nil err for get activity")
|
||||
suite.Len(doesntExistsRows, 0, "should have no rows")
|
||||
}
|
||||
|
||||
func (dt *databaseTest) TestDailyReadStats() {
|
||||
dt.Run("DailyReadStats", func(t *testing.T) {
|
||||
readStats, err := dt.dbm.Queries.GetDailyReadStats(dt.dbm.Ctx, userID)
|
||||
// MISC - TODO:
|
||||
// - (q *Queries) AddMetadata
|
||||
// - (q *Queries) GetDailyReadStats
|
||||
// - (q *Queries) GetDatabaseInfo
|
||||
// - (q *Queries) UpdateSettings
|
||||
func (suite *DatabaseTestSuite) TestGetDailyReadStats() {
|
||||
readStats, err := suite.dbm.Queries.GetDailyReadStats(suite.dbm.Ctx, userID)
|
||||
|
||||
assert.Nil(t, err, "should have nil err")
|
||||
assert.Len(t, readStats, 30, "should have length of 30")
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(readStats, 30, "should have length of 30")
|
||||
|
||||
// Validate 1 Minute / Day - Last 10 Days
|
||||
for i := 0; i < 10; i++ {
|
||||
stat := readStats[i]
|
||||
assert.Equal(t, int64(1), stat.MinutesRead, "should have one minute read")
|
||||
suite.Equal(int64(1), stat.MinutesRead, "should have one minute read")
|
||||
}
|
||||
|
||||
// Validate 0 Minute / Day - Remaining 20 Days
|
||||
for i := 10; i < 30; i++ {
|
||||
stat := readStats[i]
|
||||
assert.Equal(t, int64(0), stat.MinutesRead, "should have zero minutes read")
|
||||
suite.Equal(int64(0), stat.MinutesRead, "should have zero minutes read")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
58
database/migrations/20240311121111_user_timezone.go
Normal file
58
database/migrations/20240311121111_user_timezone.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigrationContext(upUserTimezone, downUserTimezone)
|
||||
}
|
||||
|
||||
func upUserTimezone(ctx context.Context, tx *sql.Tx) error {
|
||||
// Determine if we have a new DB or not
|
||||
isNew := ctx.Value("isNew").(bool)
|
||||
if isNew {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy table & create column
|
||||
_, err := tx.Exec(`
|
||||
-- Copy Table
|
||||
CREATE TABLE temp_users AS SELECT * FROM users;
|
||||
ALTER TABLE temp_users DROP COLUMN time_offset;
|
||||
ALTER TABLE temp_users ADD COLUMN timezone TEXT;
|
||||
UPDATE temp_users SET timezone = 'Europe/London';
|
||||
|
||||
-- Clean Table
|
||||
DELETE FROM users;
|
||||
ALTER TABLE users DROP COLUMN time_offset;
|
||||
ALTER TABLE users ADD COLUMN timezone TEXT NOT NULL DEFAULT 'Europe/London';
|
||||
|
||||
-- Copy Temp Table -> Clean Table
|
||||
INSERT INTO users SELECT * FROM temp_users;
|
||||
|
||||
-- Drop Temp Table
|
||||
DROP TABLE temp_users;
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func downUserTimezone(ctx context.Context, tx *sql.Tx) error {
|
||||
// Update column name & value
|
||||
_, err := tx.Exec(`
|
||||
ALTER TABLE users RENAME COLUMN timezone TO time_offset;
|
||||
UPDATE users SET time_offset = '0 hours';
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
38
database/migrations/20240510123707_import_basepath.go
Normal file
38
database/migrations/20240510123707_import_basepath.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigrationContext(upImportBasepath, downImportBasepath)
|
||||
}
|
||||
|
||||
func upImportBasepath(ctx context.Context, tx *sql.Tx) error {
|
||||
// Determine if we have a new DB or not
|
||||
isNew := ctx.Value("isNew").(bool)
|
||||
if isNew {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add basepath column
|
||||
_, err := tx.Exec(`ALTER TABLE documents ADD COLUMN basepath TEXT;`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This code is executed when the migration is applied.
|
||||
return nil
|
||||
}
|
||||
|
||||
func downImportBasepath(ctx context.Context, tx *sql.Tx) error {
|
||||
// Drop basepath column
|
||||
_, err := tx.Exec("ALTER documents DROP COLUMN basepath;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.27.0
|
||||
|
||||
package database
|
||||
|
||||
import ()
|
||||
|
||||
type Activity struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
@@ -30,6 +28,7 @@ type Device struct {
|
||||
type Document struct {
|
||||
ID string `json:"id"`
|
||||
Md5 *string `json:"md5"`
|
||||
Basepath *string `json:"basepath"`
|
||||
Filepath *string `json:"filepath"`
|
||||
Coverfile *string `json:"coverfile"`
|
||||
Title *string `json:"title"`
|
||||
@@ -63,6 +62,7 @@ type DocumentUserStatistic struct {
|
||||
UserID string `json:"user_id"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
LastRead string `json:"last_read"`
|
||||
LastSeen string `json:"last_seen"`
|
||||
ReadPercentage float64 `json:"read_percentage"`
|
||||
TotalTimeSeconds int64 `json:"total_time_seconds"`
|
||||
TotalWordsRead int64 `json:"total_words_read"`
|
||||
@@ -103,7 +103,7 @@ type User struct {
|
||||
Pass *string `json:"-"`
|
||||
AuthHash *string `json:"auth_hash"`
|
||||
Admin bool `json:"-"`
|
||||
TimeOffset *string `json:"time_offset"`
|
||||
Timezone *string `json:"timezone"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
@@ -116,35 +116,8 @@ type UserStreak struct {
|
||||
CurrentStreak int64 `json:"current_streak"`
|
||||
CurrentStreakStartDate string `json:"current_streak_start_date"`
|
||||
CurrentStreakEndDate string `json:"current_streak_end_date"`
|
||||
}
|
||||
|
||||
type ViewDocumentUserStatistic struct {
|
||||
DocumentID string `json:"document_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
LastRead interface{} `json:"last_read"`
|
||||
ReadPercentage *float64 `json:"read_percentage"`
|
||||
TotalTimeSeconds *float64 `json:"total_time_seconds"`
|
||||
TotalWordsRead interface{} `json:"total_words_read"`
|
||||
TotalWpm int64 `json:"total_wpm"`
|
||||
YearlyTimeSeconds *float64 `json:"yearly_time_seconds"`
|
||||
YearlyWordsRead interface{} `json:"yearly_words_read"`
|
||||
YearlyWpm interface{} `json:"yearly_wpm"`
|
||||
MonthlyTimeSeconds *float64 `json:"monthly_time_seconds"`
|
||||
MonthlyWordsRead interface{} `json:"monthly_words_read"`
|
||||
MonthlyWpm interface{} `json:"monthly_wpm"`
|
||||
WeeklyTimeSeconds *float64 `json:"weekly_time_seconds"`
|
||||
WeeklyWordsRead interface{} `json:"weekly_words_read"`
|
||||
WeeklyWpm interface{} `json:"weekly_wpm"`
|
||||
}
|
||||
|
||||
type ViewUserStreak struct {
|
||||
UserID string `json:"user_id"`
|
||||
Window string `json:"window"`
|
||||
MaxStreak interface{} `json:"max_streak"`
|
||||
MaxStreakStartDate interface{} `json:"max_streak_start_date"`
|
||||
MaxStreakEndDate interface{} `json:"max_streak_end_date"`
|
||||
CurrentStreak interface{} `json:"current_streak"`
|
||||
CurrentStreakStartDate interface{} `json:"current_streak_start_date"`
|
||||
CurrentStreakEndDate interface{} `json:"current_streak_end_date"`
|
||||
LastTimezone string `json:"last_timezone"`
|
||||
LastSeen string `json:"last_seen"`
|
||||
LastRecord string `json:"last_record"`
|
||||
LastCalculated string `json:"last_calculated"`
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ INSERT INTO users (id, pass, auth_hash, admin)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- name: DeleteUser :execrows
|
||||
DELETE FROM users WHERE id = $id;
|
||||
|
||||
-- name: DeleteDocument :execrows
|
||||
UPDATE documents
|
||||
SET
|
||||
@@ -64,7 +67,7 @@ WITH filtered_activity AS (
|
||||
SELECT
|
||||
document_id,
|
||||
device_id,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', activity.start_time, users.time_offset) AS TEXT) AS start_time,
|
||||
LOCAL_TIME(activity.start_time, users.timezone) AS start_time,
|
||||
title,
|
||||
author,
|
||||
duration,
|
||||
@@ -77,7 +80,7 @@ LEFT JOIN users ON users.id = activity.user_id;
|
||||
|
||||
-- name: GetDailyReadStats :many
|
||||
WITH RECURSIVE last_30_days AS (
|
||||
SELECT DATE('now', time_offset) AS date
|
||||
SELECT LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone) AS date
|
||||
FROM users WHERE users.id = $user_id
|
||||
UNION ALL
|
||||
SELECT DATE(date, '-1 days')
|
||||
@@ -96,7 +99,7 @@ filtered_activity AS (
|
||||
activity_days AS (
|
||||
SELECT
|
||||
SUM(duration) AS seconds_read,
|
||||
DATE(start_time, time_offset) AS day
|
||||
LOCAL_DATE(start_time, timezone) AS day
|
||||
FROM filtered_activity AS activity
|
||||
LEFT JOIN users ON users.id = activity.user_id
|
||||
GROUP BY day
|
||||
@@ -135,8 +138,8 @@ WHERE id = $device_id LIMIT 1;
|
||||
SELECT
|
||||
devices.id,
|
||||
devices.device_name,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', devices.created_at, users.time_offset) AS TEXT) AS created_at,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', devices.last_synced, users.time_offset) AS TEXT) AS last_synced
|
||||
LOCAL_TIME(devices.created_at, users.timezone) AS created_at,
|
||||
LOCAL_TIME(devices.last_synced, users.timezone) AS last_synced
|
||||
FROM devices
|
||||
JOIN users ON users.id = devices.user_id
|
||||
WHERE users.id = $user_id
|
||||
@@ -174,7 +177,7 @@ SELECT
|
||||
CAST(COALESCE(dus.total_wpm, 0.0) AS INTEGER) AS wpm,
|
||||
COALESCE(dus.read_percentage, 0) AS read_percentage,
|
||||
COALESCE(dus.total_time_seconds, 0) AS total_time_seconds,
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', COALESCE(dus.last_read, "1970-01-01"), users.time_offset)
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(COALESCE(dus.last_read, STRFTIME('%Y-%m-%dT%H:%M:%SZ', 0, 'unixepoch')), users.timezone))
|
||||
AS last_read,
|
||||
ROUND(CAST(CASE
|
||||
WHEN dus.percentage IS NULL THEN 0.0
|
||||
@@ -226,7 +229,7 @@ SELECT
|
||||
CAST(COALESCE(dus.total_wpm, 0.0) AS INTEGER) AS wpm,
|
||||
COALESCE(dus.read_percentage, 0) AS read_percentage,
|
||||
COALESCE(dus.total_time_seconds, 0) AS total_time_seconds,
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', COALESCE(dus.last_read, "1970-01-01"), users.time_offset)
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(COALESCE(dus.last_read, STRFTIME('%Y-%m-%dT%H:%M:%SZ', 0, 'unixepoch')), users.timezone))
|
||||
AS last_read,
|
||||
ROUND(CAST(CASE
|
||||
WHEN dus.percentage IS NULL THEN 0.0
|
||||
@@ -280,7 +283,7 @@ SELECT
|
||||
ROUND(CAST(progress.percentage AS REAL) * 100, 2) AS percentage,
|
||||
progress.document_id,
|
||||
progress.user_id,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', progress.created_at, users.time_offset) AS TEXT) AS created_at
|
||||
LOCAL_TIME(progress.created_at, users.timezone) AS created_at
|
||||
FROM document_progress AS progress
|
||||
LEFT JOIN users ON progress.user_id = users.id
|
||||
LEFT JOIN devices ON progress.device_id = devices.id
|
||||
@@ -369,7 +372,8 @@ UPDATE users
|
||||
SET
|
||||
pass = COALESCE($password, pass),
|
||||
auth_hash = COALESCE($auth_hash, auth_hash),
|
||||
time_offset = COALESCE($time_offset, time_offset)
|
||||
timezone = COALESCE($timezone, timezone),
|
||||
admin = COALESCE($admin, admin)
|
||||
WHERE id = $user_id
|
||||
RETURNING *;
|
||||
|
||||
@@ -395,6 +399,7 @@ RETURNING *;
|
||||
INSERT INTO documents (
|
||||
id,
|
||||
md5,
|
||||
basepath,
|
||||
filepath,
|
||||
coverfile,
|
||||
title,
|
||||
@@ -409,10 +414,11 @@ INSERT INTO documents (
|
||||
isbn10,
|
||||
isbn13
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET
|
||||
md5 = COALESCE(excluded.md5, md5),
|
||||
basepath = COALESCE(excluded.basepath, basepath),
|
||||
filepath = COALESCE(excluded.filepath, filepath),
|
||||
coverfile = COALESCE(excluded.coverfile, coverfile),
|
||||
title = COALESCE(excluded.title, title),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.27.0
|
||||
// source: query.sql
|
||||
|
||||
package database
|
||||
@@ -153,6 +153,18 @@ func (q *Queries) DeleteDocument(ctx context.Context, id string) (int64, error)
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
const deleteUser = `-- name: DeleteUser :execrows
|
||||
DELETE FROM users WHERE id = ?1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteUser(ctx context.Context, id string) (int64, error) {
|
||||
result, err := q.db.ExecContext(ctx, deleteUser, id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
const getActivity = `-- name: GetActivity :many
|
||||
WITH filtered_activity AS (
|
||||
SELECT
|
||||
@@ -181,7 +193,7 @@ WITH filtered_activity AS (
|
||||
SELECT
|
||||
document_id,
|
||||
device_id,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', activity.start_time, users.time_offset) AS TEXT) AS start_time,
|
||||
LOCAL_TIME(activity.start_time, users.timezone) AS start_time,
|
||||
title,
|
||||
author,
|
||||
duration,
|
||||
@@ -204,7 +216,7 @@ type GetActivityParams struct {
|
||||
type GetActivityRow struct {
|
||||
DocumentID string `json:"document_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
StartTime string `json:"start_time"`
|
||||
StartTime interface{} `json:"start_time"`
|
||||
Title *string `json:"title"`
|
||||
Author *string `json:"author"`
|
||||
Duration int64 `json:"duration"`
|
||||
@@ -254,7 +266,7 @@ func (q *Queries) GetActivity(ctx context.Context, arg GetActivityParams) ([]Get
|
||||
|
||||
const getDailyReadStats = `-- name: GetDailyReadStats :many
|
||||
WITH RECURSIVE last_30_days AS (
|
||||
SELECT DATE('now', time_offset) AS date
|
||||
SELECT LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone) AS date
|
||||
FROM users WHERE users.id = ?1
|
||||
UNION ALL
|
||||
SELECT DATE(date, '-1 days')
|
||||
@@ -273,7 +285,7 @@ filtered_activity AS (
|
||||
activity_days AS (
|
||||
SELECT
|
||||
SUM(duration) AS seconds_read,
|
||||
DATE(start_time, time_offset) AS day
|
||||
LOCAL_DATE(start_time, timezone) AS day
|
||||
FROM filtered_activity AS activity
|
||||
LEFT JOIN users ON users.id = activity.user_id
|
||||
GROUP BY day
|
||||
@@ -410,8 +422,8 @@ const getDevices = `-- name: GetDevices :many
|
||||
SELECT
|
||||
devices.id,
|
||||
devices.device_name,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', devices.created_at, users.time_offset) AS TEXT) AS created_at,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', devices.last_synced, users.time_offset) AS TEXT) AS last_synced
|
||||
LOCAL_TIME(devices.created_at, users.timezone) AS created_at,
|
||||
LOCAL_TIME(devices.last_synced, users.timezone) AS last_synced
|
||||
FROM devices
|
||||
JOIN users ON users.id = devices.user_id
|
||||
WHERE users.id = ?1
|
||||
@@ -421,8 +433,8 @@ ORDER BY devices.last_synced DESC
|
||||
type GetDevicesRow struct {
|
||||
ID string `json:"id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastSynced string `json:"last_synced"`
|
||||
CreatedAt interface{} `json:"created_at"`
|
||||
LastSynced interface{} `json:"last_synced"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetDevices(ctx context.Context, userID string) ([]GetDevicesRow, error) {
|
||||
@@ -454,7 +466,7 @@ func (q *Queries) GetDevices(ctx context.Context, userID string) ([]GetDevicesRo
|
||||
}
|
||||
|
||||
const getDocument = `-- name: GetDocument :one
|
||||
SELECT id, md5, filepath, coverfile, title, author, series, series_index, lang, description, words, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at FROM documents
|
||||
SELECT id, md5, basepath, filepath, coverfile, title, author, series, series_index, lang, description, words, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at FROM documents
|
||||
WHERE id = ?1 LIMIT 1
|
||||
`
|
||||
|
||||
@@ -464,6 +476,7 @@ func (q *Queries) GetDocument(ctx context.Context, documentID string) (Document,
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Basepath,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
@@ -544,7 +557,7 @@ SELECT
|
||||
CAST(COALESCE(dus.total_wpm, 0.0) AS INTEGER) AS wpm,
|
||||
COALESCE(dus.read_percentage, 0) AS read_percentage,
|
||||
COALESCE(dus.total_time_seconds, 0) AS total_time_seconds,
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', COALESCE(dus.last_read, "1970-01-01"), users.time_offset)
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(COALESCE(dus.last_read, STRFTIME('%Y-%m-%dT%H:%M:%SZ', 0, 'unixepoch')), users.timezone))
|
||||
AS last_read,
|
||||
ROUND(CAST(CASE
|
||||
WHEN dus.percentage IS NULL THEN 0.0
|
||||
@@ -612,7 +625,7 @@ func (q *Queries) GetDocumentWithStats(ctx context.Context, arg GetDocumentWithS
|
||||
}
|
||||
|
||||
const getDocuments = `-- name: GetDocuments :many
|
||||
SELECT id, md5, filepath, coverfile, title, author, series, series_index, lang, description, words, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at FROM documents
|
||||
SELECT id, md5, basepath, filepath, coverfile, title, author, series, series_index, lang, description, words, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at FROM documents
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?2
|
||||
OFFSET ?1
|
||||
@@ -635,6 +648,7 @@ func (q *Queries) GetDocuments(ctx context.Context, arg GetDocumentsParams) ([]D
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Basepath,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
@@ -698,7 +712,7 @@ SELECT
|
||||
CAST(COALESCE(dus.total_wpm, 0.0) AS INTEGER) AS wpm,
|
||||
COALESCE(dus.read_percentage, 0) AS read_percentage,
|
||||
COALESCE(dus.total_time_seconds, 0) AS total_time_seconds,
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', COALESCE(dus.last_read, "1970-01-01"), users.time_offset)
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(COALESCE(dus.last_read, STRFTIME('%Y-%m-%dT%H:%M:%SZ', 0, 'unixepoch')), users.timezone))
|
||||
AS last_read,
|
||||
ROUND(CAST(CASE
|
||||
WHEN dus.percentage IS NULL THEN 0.0
|
||||
@@ -819,7 +833,7 @@ func (q *Queries) GetLastActivity(ctx context.Context, arg GetLastActivityParams
|
||||
}
|
||||
|
||||
const getMissingDocuments = `-- name: GetMissingDocuments :many
|
||||
SELECT documents.id, documents.md5, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.words, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at FROM documents
|
||||
SELECT documents.id, documents.md5, documents.basepath, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.words, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at FROM documents
|
||||
WHERE
|
||||
documents.filepath IS NOT NULL
|
||||
AND documents.deleted = false
|
||||
@@ -848,6 +862,7 @@ func (q *Queries) GetMissingDocuments(ctx context.Context, documentIds []string)
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Basepath,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
@@ -887,7 +902,7 @@ SELECT
|
||||
ROUND(CAST(progress.percentage AS REAL) * 100, 2) AS percentage,
|
||||
progress.document_id,
|
||||
progress.user_id,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', progress.created_at, users.time_offset) AS TEXT) AS created_at
|
||||
LOCAL_TIME(progress.created_at, users.timezone) AS created_at
|
||||
FROM document_progress AS progress
|
||||
LEFT JOIN users ON progress.user_id = users.id
|
||||
LEFT JOIN devices ON progress.device_id = devices.id
|
||||
@@ -920,7 +935,7 @@ type GetProgressRow struct {
|
||||
Percentage float64 `json:"percentage"`
|
||||
DocumentID string `json:"document_id"`
|
||||
UserID string `json:"user_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
CreatedAt interface{} `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetProgress(ctx context.Context, arg GetProgressParams) ([]GetProgressRow, error) {
|
||||
@@ -961,7 +976,7 @@ func (q *Queries) GetProgress(ctx context.Context, arg GetProgressParams) ([]Get
|
||||
}
|
||||
|
||||
const getUser = `-- name: GetUser :one
|
||||
SELECT id, pass, auth_hash, admin, time_offset, created_at FROM users
|
||||
SELECT id, pass, auth_hash, admin, timezone, created_at FROM users
|
||||
WHERE id = ?1 LIMIT 1
|
||||
`
|
||||
|
||||
@@ -973,7 +988,7 @@ func (q *Queries) GetUser(ctx context.Context, userID string) (User, error) {
|
||||
&i.Pass,
|
||||
&i.AuthHash,
|
||||
&i.Admin,
|
||||
&i.TimeOffset,
|
||||
&i.Timezone,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
@@ -1063,7 +1078,7 @@ func (q *Queries) GetUserStatistics(ctx context.Context) ([]GetUserStatisticsRow
|
||||
}
|
||||
|
||||
const getUserStreaks = `-- name: GetUserStreaks :many
|
||||
SELECT user_id, "window", max_streak, max_streak_start_date, max_streak_end_date, current_streak, current_streak_start_date, current_streak_end_date FROM user_streaks
|
||||
SELECT user_id, "window", max_streak, max_streak_start_date, max_streak_end_date, current_streak, current_streak_start_date, current_streak_end_date, last_timezone, last_seen, last_record, last_calculated FROM user_streaks
|
||||
WHERE user_id = ?1
|
||||
`
|
||||
|
||||
@@ -1085,6 +1100,10 @@ func (q *Queries) GetUserStreaks(ctx context.Context, userID string) ([]UserStre
|
||||
&i.CurrentStreak,
|
||||
&i.CurrentStreakStartDate,
|
||||
&i.CurrentStreakEndDate,
|
||||
&i.LastTimezone,
|
||||
&i.LastSeen,
|
||||
&i.LastRecord,
|
||||
&i.LastCalculated,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1100,7 +1119,7 @@ func (q *Queries) GetUserStreaks(ctx context.Context, userID string) ([]UserStre
|
||||
}
|
||||
|
||||
const getUsers = `-- name: GetUsers :many
|
||||
SELECT id, pass, auth_hash, admin, time_offset, created_at FROM users
|
||||
SELECT id, pass, auth_hash, admin, timezone, created_at FROM users
|
||||
`
|
||||
|
||||
func (q *Queries) GetUsers(ctx context.Context) ([]User, error) {
|
||||
@@ -1117,7 +1136,7 @@ func (q *Queries) GetUsers(ctx context.Context) ([]User, error) {
|
||||
&i.Pass,
|
||||
&i.AuthHash,
|
||||
&i.Admin,
|
||||
&i.TimeOffset,
|
||||
&i.Timezone,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -1251,15 +1270,17 @@ UPDATE users
|
||||
SET
|
||||
pass = COALESCE(?1, pass),
|
||||
auth_hash = COALESCE(?2, auth_hash),
|
||||
time_offset = COALESCE(?3, time_offset)
|
||||
WHERE id = ?4
|
||||
RETURNING id, pass, auth_hash, admin, time_offset, created_at
|
||||
timezone = COALESCE(?3, timezone),
|
||||
admin = COALESCE(?4, admin)
|
||||
WHERE id = ?5
|
||||
RETURNING id, pass, auth_hash, admin, timezone, created_at
|
||||
`
|
||||
|
||||
type UpdateUserParams struct {
|
||||
Password *string `json:"-"`
|
||||
AuthHash *string `json:"auth_hash"`
|
||||
TimeOffset *string `json:"time_offset"`
|
||||
Timezone *string `json:"timezone"`
|
||||
Admin bool `json:"-"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
@@ -1267,7 +1288,8 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, e
|
||||
row := q.db.QueryRowContext(ctx, updateUser,
|
||||
arg.Password,
|
||||
arg.AuthHash,
|
||||
arg.TimeOffset,
|
||||
arg.Timezone,
|
||||
arg.Admin,
|
||||
arg.UserID,
|
||||
)
|
||||
var i User
|
||||
@@ -1276,7 +1298,7 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, e
|
||||
&i.Pass,
|
||||
&i.AuthHash,
|
||||
&i.Admin,
|
||||
&i.TimeOffset,
|
||||
&i.Timezone,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
@@ -1322,6 +1344,7 @@ const upsertDocument = `-- name: UpsertDocument :one
|
||||
INSERT INTO documents (
|
||||
id,
|
||||
md5,
|
||||
basepath,
|
||||
filepath,
|
||||
coverfile,
|
||||
title,
|
||||
@@ -1336,10 +1359,11 @@ INSERT INTO documents (
|
||||
isbn10,
|
||||
isbn13
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET
|
||||
md5 = COALESCE(excluded.md5, md5),
|
||||
basepath = COALESCE(excluded.basepath, basepath),
|
||||
filepath = COALESCE(excluded.filepath, filepath),
|
||||
coverfile = COALESCE(excluded.coverfile, coverfile),
|
||||
title = COALESCE(excluded.title, title),
|
||||
@@ -1353,12 +1377,13 @@ SET
|
||||
gbid = COALESCE(excluded.gbid, gbid),
|
||||
isbn10 = COALESCE(excluded.isbn10, isbn10),
|
||||
isbn13 = COALESCE(excluded.isbn13, isbn13)
|
||||
RETURNING id, md5, filepath, coverfile, title, author, series, series_index, lang, description, words, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at
|
||||
RETURNING id, md5, basepath, filepath, coverfile, title, author, series, series_index, lang, description, words, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at
|
||||
`
|
||||
|
||||
type UpsertDocumentParams struct {
|
||||
ID string `json:"id"`
|
||||
Md5 *string `json:"md5"`
|
||||
Basepath *string `json:"basepath"`
|
||||
Filepath *string `json:"filepath"`
|
||||
Coverfile *string `json:"coverfile"`
|
||||
Title *string `json:"title"`
|
||||
@@ -1378,6 +1403,7 @@ func (q *Queries) UpsertDocument(ctx context.Context, arg UpsertDocumentParams)
|
||||
row := q.db.QueryRowContext(ctx, upsertDocument,
|
||||
arg.ID,
|
||||
arg.Md5,
|
||||
arg.Basepath,
|
||||
arg.Filepath,
|
||||
arg.Coverfile,
|
||||
arg.Title,
|
||||
@@ -1396,6 +1422,7 @@ func (q *Queries) UpsertDocument(ctx context.Context, arg UpsertDocumentParams)
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Basepath,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
|
||||
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
pass TEXT NOT NULL,
|
||||
auth_hash TEXT NOT NULL,
|
||||
admin BOOLEAN NOT NULL DEFAULT 0 CHECK (admin IN (0, 1)),
|
||||
time_offset TEXT NOT NULL DEFAULT '0 hours',
|
||||
timezone TEXT NOT NULL DEFAULT 'Europe/London',
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||
);
|
||||
@@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS documents (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
|
||||
md5 TEXT,
|
||||
basepath TEXT,
|
||||
filepath TEXT,
|
||||
coverfile TEXT,
|
||||
title TEXT,
|
||||
@@ -117,30 +118,13 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
created_at DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||
);
|
||||
|
||||
---------------------------------------------------------------
|
||||
----------------------- Temporary Tables ----------------------
|
||||
---------------------------------------------------------------
|
||||
|
||||
-- Temporary User Streaks Table (Cached from View)
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS user_streaks (
|
||||
user_id TEXT NOT NULL,
|
||||
window TEXT NOT NULL,
|
||||
|
||||
max_streak INTEGER NOT NULL,
|
||||
max_streak_start_date TEXT NOT NULL,
|
||||
max_streak_end_date TEXT NOT NULL,
|
||||
|
||||
current_streak INTEGER NOT NULL,
|
||||
current_streak_start_date TEXT NOT NULL,
|
||||
current_streak_end_date TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Temporary Document User Statistics Table (Cached from View)
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS document_user_statistics (
|
||||
-- Document User Statistics Table
|
||||
CREATE TABLE IF NOT EXISTS document_user_statistics (
|
||||
document_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
percentage REAL NOT NULL,
|
||||
last_read TEXT NOT NULL,
|
||||
last_read DATETIME NOT NULL,
|
||||
last_seen DATETIME NOT NULL,
|
||||
read_percentage REAL NOT NULL,
|
||||
|
||||
total_time_seconds INTEGER NOT NULL,
|
||||
@@ -162,321 +146,39 @@ CREATE TEMPORARY TABLE IF NOT EXISTS document_user_statistics (
|
||||
UNIQUE(document_id, user_id) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
-- User Streaks Table
|
||||
CREATE TABLE IF NOT EXISTS user_streaks (
|
||||
user_id TEXT NOT NULL,
|
||||
window TEXT NOT NULL,
|
||||
|
||||
max_streak INTEGER NOT NULL,
|
||||
max_streak_start_date TEXT NOT NULL,
|
||||
max_streak_end_date TEXT NOT NULL,
|
||||
|
||||
current_streak INTEGER NOT NULL,
|
||||
current_streak_start_date TEXT NOT NULL,
|
||||
current_streak_end_date TEXT NOT NULL,
|
||||
|
||||
last_timezone TEXT NOT NULL,
|
||||
last_seen TEXT NOT NULL,
|
||||
last_record TEXT NOT NULL,
|
||||
last_calculated TEXT NOT NULL,
|
||||
|
||||
UNIQUE(user_id, window) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
---------------------------------------------------------------
|
||||
--------------------------- Indexes ---------------------------
|
||||
---------------------------------------------------------------
|
||||
|
||||
CREATE INDEX IF NOT EXISTS activity_start_time ON activity (start_time);
|
||||
CREATE INDEX IF NOT EXISTS activity_created_at ON activity (created_at);
|
||||
CREATE INDEX IF NOT EXISTS activity_user_id ON activity (user_id);
|
||||
CREATE INDEX IF NOT EXISTS activity_user_id_document_id ON activity (
|
||||
user_id,
|
||||
document_id
|
||||
);
|
||||
|
||||
|
||||
---------------------------------------------------------------
|
||||
---------------------------- Views ----------------------------
|
||||
---------------------------------------------------------------
|
||||
|
||||
DROP VIEW IF EXISTS view_user_streaks;
|
||||
DROP VIEW IF EXISTS view_document_user_statistics;
|
||||
|
||||
--------------------------------
|
||||
--------- User Streaks ---------
|
||||
--------------------------------
|
||||
|
||||
CREATE VIEW view_user_streaks AS
|
||||
|
||||
WITH document_windows AS (
|
||||
SELECT
|
||||
activity.user_id,
|
||||
users.time_offset,
|
||||
DATE(
|
||||
activity.start_time,
|
||||
users.time_offset,
|
||||
'weekday 0', '-7 day'
|
||||
) AS weekly_read,
|
||||
DATE(activity.start_time, users.time_offset) AS daily_read
|
||||
FROM activity
|
||||
LEFT JOIN users ON users.id = activity.user_id
|
||||
GROUP BY activity.user_id, weekly_read, daily_read
|
||||
),
|
||||
weekly_partitions AS (
|
||||
SELECT
|
||||
user_id,
|
||||
time_offset,
|
||||
'WEEK' AS "window",
|
||||
weekly_read AS read_window,
|
||||
row_number() OVER (
|
||||
PARTITION BY user_id ORDER BY weekly_read DESC
|
||||
) AS seqnum
|
||||
FROM document_windows
|
||||
GROUP BY user_id, weekly_read
|
||||
),
|
||||
daily_partitions AS (
|
||||
SELECT
|
||||
user_id,
|
||||
time_offset,
|
||||
'DAY' AS "window",
|
||||
daily_read AS read_window,
|
||||
row_number() OVER (
|
||||
PARTITION BY user_id ORDER BY daily_read DESC
|
||||
) AS seqnum
|
||||
FROM document_windows
|
||||
GROUP BY user_id, daily_read
|
||||
),
|
||||
streaks AS (
|
||||
SELECT
|
||||
COUNT(*) AS streak,
|
||||
MIN(read_window) AS start_date,
|
||||
MAX(read_window) AS end_date,
|
||||
window,
|
||||
user_id,
|
||||
time_offset
|
||||
FROM daily_partitions
|
||||
GROUP BY
|
||||
time_offset,
|
||||
user_id,
|
||||
DATE(read_window, '+' || seqnum || ' day')
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
COUNT(*) AS streak,
|
||||
MIN(read_window) AS start_date,
|
||||
MAX(read_window) AS end_date,
|
||||
window,
|
||||
user_id,
|
||||
time_offset
|
||||
FROM weekly_partitions
|
||||
GROUP BY
|
||||
time_offset,
|
||||
user_id,
|
||||
DATE(read_window, '+' || (seqnum * 7) || ' day')
|
||||
),
|
||||
max_streak AS (
|
||||
SELECT
|
||||
MAX(streak) AS max_streak,
|
||||
start_date AS max_streak_start_date,
|
||||
end_date AS max_streak_end_date,
|
||||
window,
|
||||
user_id
|
||||
FROM streaks
|
||||
GROUP BY user_id, window
|
||||
),
|
||||
current_streak AS (
|
||||
SELECT
|
||||
streak AS current_streak,
|
||||
start_date AS current_streak_start_date,
|
||||
end_date AS current_streak_end_date,
|
||||
window,
|
||||
user_id
|
||||
FROM streaks
|
||||
WHERE CASE
|
||||
WHEN window = "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 window = "DAY" THEN
|
||||
DATE('now', time_offset, '-1 day') = current_streak_end_date
|
||||
OR DATE('now', time_offset) = current_streak_end_date
|
||||
END
|
||||
GROUP BY user_id, window
|
||||
)
|
||||
SELECT
|
||||
max_streak.user_id,
|
||||
max_streak.window,
|
||||
IFNULL(max_streak, 0) AS max_streak,
|
||||
IFNULL(max_streak_start_date, "N/A") AS max_streak_start_date,
|
||||
IFNULL(max_streak_end_date, "N/A") AS max_streak_end_date,
|
||||
IFNULL(current_streak, 0) AS current_streak,
|
||||
IFNULL(current_streak_start_date, "N/A") AS current_streak_start_date,
|
||||
IFNULL(current_streak_end_date, "N/A") AS current_streak_end_date
|
||||
FROM max_streak
|
||||
LEFT JOIN current_streak ON
|
||||
current_streak.user_id = max_streak.user_id
|
||||
AND current_streak.window = max_streak.window;
|
||||
|
||||
--------------------------------
|
||||
------- Document Stats ---------
|
||||
--------------------------------
|
||||
|
||||
CREATE VIEW view_document_user_statistics AS
|
||||
|
||||
WITH intermediate_ga AS (
|
||||
SELECT
|
||||
ga1.id AS row_id,
|
||||
ga1.user_id,
|
||||
ga1.document_id,
|
||||
ga1.duration,
|
||||
ga1.start_time,
|
||||
ga1.start_percentage,
|
||||
ga1.end_percentage,
|
||||
|
||||
-- Find Overlapping Events (Assign Unique ID)
|
||||
(
|
||||
SELECT MIN(id)
|
||||
FROM activity AS ga2
|
||||
WHERE
|
||||
ga1.document_id = ga2.document_id
|
||||
AND ga1.user_id = ga2.user_id
|
||||
AND ga1.start_percentage <= ga2.end_percentage
|
||||
AND ga1.end_percentage >= ga2.start_percentage
|
||||
) AS group_leader
|
||||
FROM activity AS ga1
|
||||
),
|
||||
|
||||
grouped_activity AS (
|
||||
SELECT
|
||||
user_id,
|
||||
document_id,
|
||||
MAX(start_time) AS start_time,
|
||||
MIN(start_percentage) AS start_percentage,
|
||||
MAX(end_percentage) AS end_percentage,
|
||||
MAX(end_percentage) - MIN(start_percentage) AS read_percentage,
|
||||
SUM(duration) AS duration
|
||||
FROM intermediate_ga
|
||||
GROUP BY group_leader
|
||||
),
|
||||
|
||||
current_progress AS (
|
||||
SELECT
|
||||
user_id,
|
||||
document_id,
|
||||
COALESCE((
|
||||
SELECT percentage
|
||||
FROM document_progress AS dp
|
||||
WHERE
|
||||
dp.user_id = iga.user_id
|
||||
AND dp.document_id = iga.document_id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
), end_percentage) AS percentage
|
||||
FROM intermediate_ga AS iga
|
||||
GROUP BY user_id, document_id
|
||||
HAVING MAX(start_time)
|
||||
)
|
||||
|
||||
SELECT
|
||||
ga.document_id,
|
||||
ga.user_id,
|
||||
cp.percentage,
|
||||
MAX(start_time) AS last_read,
|
||||
SUM(read_percentage) AS read_percentage,
|
||||
|
||||
-- All Time WPM
|
||||
SUM(duration) AS total_time_seconds,
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(read_percentage))
|
||||
AS total_words_read,
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(read_percentage))
|
||||
/ (SUM(duration) / 60.0) AS total_wpm,
|
||||
|
||||
-- Yearly WPM
|
||||
SUM(CASE WHEN start_time >= DATE('now', '-1 year') THEN duration ELSE 0 END)
|
||||
AS yearly_time_seconds,
|
||||
(
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||
* SUM(
|
||||
CASE
|
||||
WHEN start_time >= DATE('now', '-1 year') THEN read_percentage
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
)
|
||||
AS yearly_words_read,
|
||||
COALESCE((
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||
* SUM(
|
||||
CASE
|
||||
WHEN start_time >= DATE('now', '-1 year') THEN read_percentage
|
||||
END
|
||||
)
|
||||
)
|
||||
/ (
|
||||
SUM(
|
||||
CASE
|
||||
WHEN start_time >= DATE('now', '-1 year') THEN duration
|
||||
END
|
||||
)
|
||||
/ 60.0
|
||||
), 0.0)
|
||||
AS yearly_wpm,
|
||||
|
||||
-- Monthly WPM
|
||||
SUM(
|
||||
CASE WHEN start_time >= DATE('now', '-1 month') THEN duration ELSE 0 END
|
||||
)
|
||||
AS monthly_time_seconds,
|
||||
(
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||
* SUM(
|
||||
CASE
|
||||
WHEN start_time >= DATE('now', '-1 month') THEN read_percentage
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
)
|
||||
AS monthly_words_read,
|
||||
COALESCE((
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||
* SUM(
|
||||
CASE
|
||||
WHEN start_time >= DATE('now', '-1 month') THEN read_percentage
|
||||
END
|
||||
)
|
||||
)
|
||||
/ (
|
||||
SUM(
|
||||
CASE
|
||||
WHEN start_time >= DATE('now', '-1 month') THEN duration
|
||||
END
|
||||
)
|
||||
/ 60.0
|
||||
), 0.0)
|
||||
AS monthly_wpm,
|
||||
|
||||
-- Weekly WPM
|
||||
SUM(CASE WHEN start_time >= DATE('now', '-7 days') THEN duration ELSE 0 END)
|
||||
AS weekly_time_seconds,
|
||||
(
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||
* SUM(
|
||||
CASE
|
||||
WHEN start_time >= DATE('now', '-7 days') THEN read_percentage
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
)
|
||||
AS weekly_words_read,
|
||||
COALESCE((
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||
* SUM(
|
||||
CASE
|
||||
WHEN start_time >= DATE('now', '-7 days') THEN read_percentage
|
||||
END
|
||||
)
|
||||
)
|
||||
/ (
|
||||
SUM(
|
||||
CASE
|
||||
WHEN start_time >= DATE('now', '-7 days') THEN duration
|
||||
END
|
||||
)
|
||||
/ 60.0
|
||||
), 0.0)
|
||||
AS weekly_wpm
|
||||
|
||||
FROM grouped_activity AS ga
|
||||
INNER JOIN
|
||||
current_progress AS cp
|
||||
ON ga.user_id = cp.user_id AND ga.document_id = cp.document_id
|
||||
INNER JOIN
|
||||
documents AS d
|
||||
ON ga.document_id = d.id
|
||||
GROUP BY ga.document_id, ga.user_id
|
||||
ORDER BY total_wpm DESC;
|
||||
|
||||
|
||||
---------------------------------------------------------------
|
||||
--------------------------- Triggers --------------------------
|
||||
---------------------------------------------------------------
|
||||
@@ -488,3 +190,11 @@ UPDATE documents
|
||||
SET updated_at = STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||
WHERE id = old.id;
|
||||
END;
|
||||
|
||||
-- Delete User
|
||||
CREATE TRIGGER IF NOT EXISTS user_deleted
|
||||
BEFORE DELETE ON users BEGIN
|
||||
DELETE FROM activity WHERE activity.user_id=OLD.id;
|
||||
DELETE FROM devices WHERE devices.user_id=OLD.id;
|
||||
DELETE FROM document_progress WHERE document_progress.user_id=OLD.id;
|
||||
END;
|
||||
|
||||
154
database/user_streaks.sql
Normal file
154
database/user_streaks.sql
Normal file
@@ -0,0 +1,154 @@
|
||||
WITH updated_users AS (
|
||||
SELECT a.user_id
|
||||
FROM activity AS a
|
||||
LEFT JOIN users AS u ON u.id = a.user_id
|
||||
LEFT JOIN user_streaks AS s ON a.user_id = s.user_id AND s.window = 'DAY'
|
||||
WHERE
|
||||
a.created_at > COALESCE(s.last_seen, '1970-01-01')
|
||||
AND LOCAL_DATE(s.last_record, u.timezone) != LOCAL_DATE(a.start_time, u.timezone)
|
||||
GROUP BY a.user_id
|
||||
),
|
||||
|
||||
outdated_users AS (
|
||||
SELECT
|
||||
a.user_id,
|
||||
u.timezone AS last_timezone,
|
||||
MAX(a.created_at) AS last_seen,
|
||||
MAX(a.start_time) AS last_record,
|
||||
STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now') AS last_calculated
|
||||
FROM activity AS a
|
||||
LEFT JOIN users AS u ON u.id = a.user_id
|
||||
LEFT JOIN user_streaks AS s ON a.user_id = s.user_id AND s.window = 'DAY'
|
||||
GROUP BY a.user_id
|
||||
HAVING
|
||||
-- User Changed Timezones
|
||||
s.last_timezone != u.timezone
|
||||
|
||||
-- Users Date Changed
|
||||
OR LOCAL_DATE(COALESCE(s.last_calculated, '1970-01-01T00:00:00Z'), u.timezone) !=
|
||||
LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), u.timezone)
|
||||
|
||||
-- User Added New Data
|
||||
OR a.user_id IN updated_users
|
||||
),
|
||||
|
||||
document_windows AS (
|
||||
SELECT
|
||||
activity.user_id,
|
||||
users.timezone,
|
||||
DATE(
|
||||
LOCAL_DATE(activity.start_time, users.timezone),
|
||||
'weekday 0', '-7 day'
|
||||
) AS weekly_read,
|
||||
LOCAL_DATE(activity.start_time, users.timezone) AS daily_read
|
||||
FROM activity
|
||||
INNER JOIN outdated_users ON outdated_users.user_id = activity.user_id
|
||||
LEFT JOIN users ON users.id = activity.user_id
|
||||
GROUP BY activity.user_id, weekly_read, daily_read
|
||||
),
|
||||
|
||||
weekly_partitions AS (
|
||||
SELECT
|
||||
user_id,
|
||||
timezone,
|
||||
'WEEK' AS "window",
|
||||
weekly_read AS read_window,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY user_id ORDER BY weekly_read DESC
|
||||
) AS seqnum
|
||||
FROM document_windows
|
||||
GROUP BY user_id, weekly_read
|
||||
),
|
||||
|
||||
daily_partitions AS (
|
||||
SELECT
|
||||
user_id,
|
||||
timezone,
|
||||
'DAY' AS "window",
|
||||
daily_read AS read_window,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY user_id ORDER BY daily_read DESC
|
||||
) AS seqnum
|
||||
FROM document_windows
|
||||
GROUP BY user_id, daily_read
|
||||
),
|
||||
|
||||
streaks AS (
|
||||
SELECT
|
||||
COUNT(*) AS streak,
|
||||
MIN(read_window) AS start_date,
|
||||
MAX(read_window) AS end_date,
|
||||
window,
|
||||
user_id,
|
||||
timezone
|
||||
FROM daily_partitions
|
||||
GROUP BY
|
||||
timezone,
|
||||
user_id,
|
||||
DATE(read_window, '+' || seqnum || ' day')
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
COUNT(*) AS streak,
|
||||
MIN(read_window) AS start_date,
|
||||
MAX(read_window) AS end_date,
|
||||
window,
|
||||
user_id,
|
||||
timezone
|
||||
FROM weekly_partitions
|
||||
GROUP BY
|
||||
timezone,
|
||||
user_id,
|
||||
DATE(read_window, '+' || (seqnum * 7) || ' day')
|
||||
),
|
||||
|
||||
max_streak AS (
|
||||
SELECT
|
||||
MAX(streak) AS max_streak,
|
||||
start_date AS max_streak_start_date,
|
||||
end_date AS max_streak_end_date,
|
||||
window,
|
||||
user_id
|
||||
FROM streaks
|
||||
GROUP BY user_id, window
|
||||
),
|
||||
|
||||
current_streak AS (
|
||||
SELECT
|
||||
streak AS current_streak,
|
||||
start_date AS current_streak_start_date,
|
||||
end_date AS current_streak_end_date,
|
||||
window,
|
||||
user_id
|
||||
FROM streaks
|
||||
WHERE CASE
|
||||
WHEN window = "WEEK" THEN
|
||||
DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), 'weekday 0', '-14 day') = current_streak_end_date
|
||||
OR DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), 'weekday 0', '-7 day') = current_streak_end_date
|
||||
WHEN window = "DAY" THEN
|
||||
DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), '-1 day') = current_streak_end_date
|
||||
OR DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone)) = current_streak_end_date
|
||||
END
|
||||
GROUP BY user_id, window
|
||||
)
|
||||
|
||||
INSERT INTO user_streaks
|
||||
SELECT
|
||||
max_streak.user_id,
|
||||
max_streak.window,
|
||||
IFNULL(max_streak, 0) AS max_streak,
|
||||
IFNULL(max_streak_start_date, "N/A") AS max_streak_start_date,
|
||||
IFNULL(max_streak_end_date, "N/A") AS max_streak_end_date,
|
||||
IFNULL(current_streak.current_streak, 0) AS current_streak,
|
||||
IFNULL(current_streak.current_streak_start_date, "N/A") AS current_streak_start_date,
|
||||
IFNULL(current_streak.current_streak_end_date, "N/A") AS current_streak_end_date,
|
||||
outdated_users.last_timezone AS last_timezone,
|
||||
outdated_users.last_seen AS last_seen,
|
||||
outdated_users.last_record AS last_record,
|
||||
outdated_users.last_calculated AS last_calculated
|
||||
FROM max_streak
|
||||
JOIN outdated_users ON max_streak.user_id = outdated_users.user_id
|
||||
LEFT JOIN current_streak ON
|
||||
current_streak.user_id = max_streak.user_id
|
||||
AND current_streak.window = max_streak.window;
|
||||
204
database/users_test.go
Normal file
204
database/users_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"reichard.io/antholume/config"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
testUserID string = "testUser"
|
||||
testUserPass string = "testPass"
|
||||
)
|
||||
|
||||
type UsersTestSuite struct {
|
||||
suite.Suite
|
||||
dbm *DBManager
|
||||
}
|
||||
|
||||
func TestUsers(t *testing.T) {
|
||||
suite.Run(t, new(UsersTestSuite))
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) SetupTest() {
|
||||
cfg := config.Config{
|
||||
DBType: "memory",
|
||||
}
|
||||
|
||||
suite.dbm = NewMgr(&cfg)
|
||||
|
||||
// Create User
|
||||
rawAuthHash, _ := utils.GenerateToken(64)
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
_, err := suite.dbm.Queries.CreateUser(suite.dbm.Ctx, CreateUserParams{
|
||||
ID: testUserID,
|
||||
Pass: &testUserPass,
|
||||
AuthHash: &authHash,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
// Create Document
|
||||
_, err = suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
|
||||
ID: documentID,
|
||||
Title: &documentTitle,
|
||||
Author: &documentAuthor,
|
||||
Words: &documentWords,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
// Create Device
|
||||
_, err = suite.dbm.Queries.UpsertDevice(suite.dbm.Ctx, UpsertDeviceParams{
|
||||
ID: deviceID,
|
||||
UserID: testUserID,
|
||||
DeviceName: deviceName,
|
||||
})
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestGetUser() {
|
||||
user, err := suite.dbm.Queries.GetUser(suite.dbm.Ctx, testUserID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(testUserPass, *user.Pass)
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestCreateUser() {
|
||||
testUser := "user1"
|
||||
testPass := "pass1"
|
||||
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
suite.Nil(err, "should have nil err")
|
||||
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
changed, err := suite.dbm.Queries.CreateUser(suite.dbm.Ctx, CreateUserParams{
|
||||
ID: testUser,
|
||||
Pass: &testPass,
|
||||
AuthHash: &authHash,
|
||||
})
|
||||
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(int64(1), changed)
|
||||
|
||||
user, err := suite.dbm.Queries.GetUser(suite.dbm.Ctx, testUser)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(testPass, *user.Pass)
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestDeleteUser() {
|
||||
changed, err := suite.dbm.Queries.DeleteUser(suite.dbm.Ctx, testUserID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(int64(1), changed, "should have one changed row")
|
||||
|
||||
_, err = suite.dbm.Queries.GetUser(suite.dbm.Ctx, testUserID)
|
||||
suite.ErrorIs(err, sql.ErrNoRows, "should have no rows error")
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestGetUsers() {
|
||||
users, err := suite.dbm.Queries.GetUsers(suite.dbm.Ctx)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(users, 1, "should have single user")
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestUpdateUser() {
|
||||
newPassword := "newPass123"
|
||||
user, err := suite.dbm.Queries.UpdateUser(suite.dbm.Ctx, UpdateUserParams{
|
||||
UserID: testUserID,
|
||||
Password: &newPassword,
|
||||
})
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(newPassword, *user.Pass, "should have new password")
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestGetUserStatistics() {
|
||||
err := suite.dbm.CacheTempTables()
|
||||
suite.NoError(err)
|
||||
|
||||
// Ensure Zero Items
|
||||
userStats, err := suite.dbm.Queries.GetUserStatistics(suite.dbm.Ctx)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Empty(userStats, "should be empty")
|
||||
|
||||
// Create Activity
|
||||
end := time.Now()
|
||||
start := end.AddDate(0, 0, -9)
|
||||
var counter int64 = 0
|
||||
|
||||
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
|
||||
counter += 1
|
||||
|
||||
// Add Item
|
||||
activity, err := suite.dbm.Queries.AddActivity(suite.dbm.Ctx, AddActivityParams{
|
||||
DocumentID: documentID,
|
||||
DeviceID: deviceID,
|
||||
UserID: testUserID,
|
||||
StartTime: d.UTC().Format(time.RFC3339),
|
||||
Duration: 60,
|
||||
StartPercentage: float64(counter) / 100.0,
|
||||
EndPercentage: float64(counter+1) / 100.0,
|
||||
})
|
||||
|
||||
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
|
||||
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
|
||||
}
|
||||
|
||||
err = suite.dbm.CacheTempTables()
|
||||
suite.NoError(err)
|
||||
|
||||
// Ensure One Item
|
||||
userStats, err = suite.dbm.Queries.GetUserStatistics(suite.dbm.Ctx)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(userStats, 1, "should have length of one")
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestGetUsersStreaks() {
|
||||
err := suite.dbm.CacheTempTables()
|
||||
suite.NoError(err)
|
||||
|
||||
// Ensure Zero Items
|
||||
userStats, err := suite.dbm.Queries.GetUserStreaks(suite.dbm.Ctx, testUserID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Empty(userStats, "should be empty")
|
||||
|
||||
// Create Activity
|
||||
end := time.Now()
|
||||
start := end.AddDate(0, 0, -9)
|
||||
var counter int64 = 0
|
||||
|
||||
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
|
||||
counter += 1
|
||||
|
||||
// Add Item
|
||||
activity, err := suite.dbm.Queries.AddActivity(suite.dbm.Ctx, AddActivityParams{
|
||||
DocumentID: documentID,
|
||||
DeviceID: deviceID,
|
||||
UserID: testUserID,
|
||||
StartTime: d.UTC().Format(time.RFC3339),
|
||||
Duration: 60,
|
||||
StartPercentage: float64(counter) / 100.0,
|
||||
EndPercentage: float64(counter+1) / 100.0,
|
||||
})
|
||||
|
||||
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
|
||||
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
|
||||
}
|
||||
|
||||
err = suite.dbm.CacheTempTables()
|
||||
suite.NoError(err)
|
||||
|
||||
// Ensure Two Item
|
||||
userStats, err = suite.dbm.Queries.GetUserStreaks(suite.dbm.Ctx, testUserID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(userStats, 2, "should have length of two")
|
||||
|
||||
// Ensure Streak Stats
|
||||
dayStats := userStats[0]
|
||||
weekStats := userStats[1]
|
||||
suite.Equal(int64(10), dayStats.CurrentStreak, "should be 10 days")
|
||||
suite.Greater(weekStats.CurrentStreak, int64(1), "should be 2 or 3")
|
||||
}
|
||||
28
go.mod
28
go.mod
@@ -1,18 +1,24 @@
|
||||
module reichard.io/antholume
|
||||
|
||||
go 1.21
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.3
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
github.com/a-h/templ v0.3.819
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81
|
||||
github.com/gin-contrib/sessions v0.0.5
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/itchyny/gojq v0.12.14
|
||||
github.com/jarcoal/httpmock v1.3.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pressly/goose/v3 v3.17.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115
|
||||
github.com/urfave/cli/v2 v2.27.1
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
||||
@@ -21,7 +27,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/bytedance/sonic v1.10.2 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
@@ -40,7 +46,6 @@ require (
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.2.2 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
github.com/jarcoal/httpmock v1.3.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
@@ -53,19 +58,18 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sethvargo/go-retry v0.2.4 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
|
||||
79
go.sum
79
go.sum
@@ -8,23 +8,24 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
|
||||
github.com/a-h/templ v0.3.819 h1:KDJ5jTFN15FyJnmSmo2gNirIqt7hfvBD2VXVDTySckM=
|
||||
github.com/a-h/templ v0.3.819/go.mod h1:iDJKJktpttVKdWoTkRNNLcllRI+BlpopJc+8au3gOUo=
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
||||
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
@@ -94,8 +95,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -159,6 +160,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
||||
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
@@ -258,33 +261,41 @@ golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -293,17 +304,23 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -311,14 +328,18 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
|
||||
|
||||
@@ -5,10 +5,16 @@ import (
|
||||
"math"
|
||||
)
|
||||
|
||||
type SVGRawData struct {
|
||||
Value int
|
||||
Label string
|
||||
}
|
||||
|
||||
type SVGGraphPoint struct {
|
||||
X int
|
||||
Y int
|
||||
Size int
|
||||
Data SVGRawData
|
||||
}
|
||||
|
||||
type SVGGraphData struct {
|
||||
@@ -26,12 +32,12 @@ type SVGBezierOpposedLine struct {
|
||||
Angle int
|
||||
}
|
||||
|
||||
func GetSVGGraphData(inputData []int64, svgWidth int, svgHeight int) SVGGraphData {
|
||||
func GetSVGGraphData(inputData []SVGRawData, svgWidth int, svgHeight int) SVGGraphData {
|
||||
// Derive Height
|
||||
var maxHeight int = 0
|
||||
for _, item := range inputData {
|
||||
if int(item) > maxHeight {
|
||||
maxHeight = int(item)
|
||||
if int(item.Value) > maxHeight {
|
||||
maxHeight = int(item.Value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,20 +58,22 @@ func GetSVGGraphData(inputData []int64, svgWidth int, svgHeight int) SVGGraphDat
|
||||
var maxBX int = 0
|
||||
var maxBY int = 0
|
||||
var minBX int = 0
|
||||
for idx, item := range inputData {
|
||||
itemSize := int(float32(item) * sizeRatio)
|
||||
for idx, datum := range inputData {
|
||||
itemSize := int(float32(datum.Value) * sizeRatio)
|
||||
itemY := svgHeight - itemSize
|
||||
lineX := (idx + 1) * blockOffset
|
||||
barPoints = append(barPoints, SVGGraphPoint{
|
||||
X: lineX - (blockOffset / 2),
|
||||
Y: itemY,
|
||||
Size: itemSize,
|
||||
Data: datum,
|
||||
})
|
||||
|
||||
linePoints = append(linePoints, SVGGraphPoint{
|
||||
X: lineX,
|
||||
Y: itemY,
|
||||
Size: itemSize,
|
||||
Data: datum,
|
||||
})
|
||||
|
||||
if lineX > maxBX {
|
||||
@@ -115,7 +123,7 @@ func getSVGBezierControlPoint(currentPoint *SVGGraphPoint, prevPoint *SVGGraphPo
|
||||
// Modifiers
|
||||
var smoothingRatio float64 = 0.2
|
||||
var directionModifier float64 = 0
|
||||
if isReverse == true {
|
||||
if isReverse {
|
||||
directionModifier = math.Pi
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ func getGBooksMetadata(metadataSearch MetadataInfo) ([]MetadataInfo, error) {
|
||||
func saveGBooksCover(gbid string, coverFilePath string, overwrite bool) error {
|
||||
// Validate File Doesn't Exists
|
||||
_, err := os.Stat(coverFilePath)
|
||||
if err == nil && overwrite == false {
|
||||
if err == nil && !overwrite {
|
||||
log.Warn("File Alreads Exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ func hookAPI() *details {
|
||||
}
|
||||
|
||||
// Convert to JSON Response
|
||||
var responseData map[string]interface{}
|
||||
json.Unmarshal([]byte(rawResp), &responseData)
|
||||
var responseData map[string]any
|
||||
_ = json.Unmarshal([]byte(rawResp), &responseData)
|
||||
|
||||
// Return Response
|
||||
return httpmock.NewJsonResponse(200, responseData)
|
||||
|
||||
@@ -65,9 +65,9 @@ func SearchMetadata(s Source, metadataSearch MetadataInfo) ([]MetadataInfo, erro
|
||||
case SOURCE_GBOOK:
|
||||
return getGBooksMetadata(metadataSearch)
|
||||
case SOURCE_OLIB:
|
||||
return nil, errors.New("Not implemented")
|
||||
return nil, errors.New("not implemented")
|
||||
default:
|
||||
return nil, errors.New("Not implemented")
|
||||
return nil, errors.New("not implemented")
|
||||
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func GetWordCount(filepath string) (*int64, error) {
|
||||
}
|
||||
return &totalWords, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("Invalid extension")
|
||||
return nil, fmt.Errorf("Invalid extension: %s", fileExtension)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,9 @@ func GetMetadata(filepath string) (*MetadataInfo, error) {
|
||||
|
||||
// Acquire Metadata
|
||||
metadataInfo, err := handler(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to acquire metadata")
|
||||
}
|
||||
|
||||
// Calculate MD5 & Partial MD5
|
||||
partialMD5, err := utils.CalculatePartialMD5(filepath)
|
||||
|
||||
151
ngtemplates/common/utils.go
Normal file
151
ngtemplates/common/utils.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Route string
|
||||
|
||||
var (
|
||||
RouteHome Route = "HOME"
|
||||
RouteDocuments Route = "DOCUMENTS"
|
||||
RouteProgress Route = "PROGRESS"
|
||||
RouteActivity Route = "ACTIVITY"
|
||||
RouteSearch Route = "SEARCH"
|
||||
RouteAdmin Route = "ADMIN"
|
||||
RouteAdminImport Route = "ADMIN_IMPORT"
|
||||
RouteAdminUsers Route = "ADMIN_USERS"
|
||||
RouteAdminLogs Route = "ADMIN_LOGS"
|
||||
)
|
||||
|
||||
func (r Route) IsAdmin() bool {
|
||||
return strings.HasPrefix("ADMIN", string(r))
|
||||
}
|
||||
|
||||
func (r Route) Name() string {
|
||||
var pathSplit []string
|
||||
for _, rawPath := range strings.Split(string(r), "_") {
|
||||
pathLoc := strings.ToUpper(rawPath[:1]) + strings.ToLower(rawPath[1:])
|
||||
pathSplit = append(pathSplit, pathLoc)
|
||||
|
||||
}
|
||||
return strings.Join(pathSplit, " - ")
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Route Route
|
||||
User string
|
||||
Version string
|
||||
IsAdmin bool
|
||||
SearchEnabled bool
|
||||
}
|
||||
|
||||
type UserMetadata struct {
|
||||
DocumentCount int
|
||||
ActivityCount int
|
||||
ProgressCount int
|
||||
DeviceCount int
|
||||
}
|
||||
|
||||
type UserStatistics struct {
|
||||
WPM map[string][]UserStatisticEntry
|
||||
Duration map[string][]UserStatisticEntry
|
||||
Words map[string][]UserStatisticEntry
|
||||
}
|
||||
|
||||
type UserStatisticEntry struct {
|
||||
UserID string
|
||||
Value string
|
||||
}
|
||||
|
||||
// getTimeZones returns a string slice of IANA timezones.
|
||||
func GetTimeZones() []string {
|
||||
return []string{
|
||||
"Africa/Cairo",
|
||||
"Africa/Johannesburg",
|
||||
"Africa/Lagos",
|
||||
"Africa/Nairobi",
|
||||
"America/Adak",
|
||||
"America/Anchorage",
|
||||
"America/Buenos_Aires",
|
||||
"America/Chicago",
|
||||
"America/Denver",
|
||||
"America/Los_Angeles",
|
||||
"America/Mexico_City",
|
||||
"America/New_York",
|
||||
"America/Nuuk",
|
||||
"America/Phoenix",
|
||||
"America/Puerto_Rico",
|
||||
"America/Sao_Paulo",
|
||||
"America/St_Johns",
|
||||
"America/Toronto",
|
||||
"Asia/Dubai",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Kolkata",
|
||||
"Asia/Seoul",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Singapore",
|
||||
"Asia/Tokyo",
|
||||
"Atlantic/Azores",
|
||||
"Australia/Melbourne",
|
||||
"Australia/Sydney",
|
||||
"Europe/Berlin",
|
||||
"Europe/London",
|
||||
"Europe/Moscow",
|
||||
"Europe/Paris",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Honolulu",
|
||||
}
|
||||
}
|
||||
|
||||
// niceSeconds takes in an int (in seconds) and returns a string readable
|
||||
// representation. For example 1928371 -> "22d 7h 39m 31s".
|
||||
func NiceSeconds(input int64) (result string) {
|
||||
if input == 0 {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
days := math.Floor(float64(input) / 60 / 60 / 24)
|
||||
seconds := input % (60 * 60 * 24)
|
||||
hours := math.Floor(float64(seconds) / 60 / 60)
|
||||
seconds = input % (60 * 60)
|
||||
minutes := math.Floor(float64(seconds) / 60)
|
||||
seconds = input % 60
|
||||
|
||||
if days > 0 {
|
||||
result += fmt.Sprintf("%dd ", int(days))
|
||||
}
|
||||
if hours > 0 {
|
||||
result += fmt.Sprintf("%dh ", int(hours))
|
||||
}
|
||||
if minutes > 0 {
|
||||
result += fmt.Sprintf("%dm ", int(minutes))
|
||||
}
|
||||
if seconds > 0 {
|
||||
result += fmt.Sprintf("%ds", int(seconds))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// niceNumbers takes in an int and returns a string representation. For example
|
||||
// 19823 -> "19.8k".
|
||||
func NiceNumbers(input int64) string {
|
||||
if input == 0 {
|
||||
return "0"
|
||||
}
|
||||
|
||||
abbreviations := []string{"", "k", "M", "B", "T"}
|
||||
abbrevIndex := int(math.Log10(float64(input)) / 3)
|
||||
scaledNumber := float64(input) / math.Pow(10, float64(abbrevIndex*3))
|
||||
|
||||
if scaledNumber >= 100 {
|
||||
return fmt.Sprintf("%.0f%s", scaledNumber, abbreviations[abbrevIndex])
|
||||
} else if scaledNumber >= 10 {
|
||||
return fmt.Sprintf("%.1f%s", scaledNumber, abbreviations[abbrevIndex])
|
||||
} else {
|
||||
return fmt.Sprintf("%.2f%s", scaledNumber, abbreviations[abbrevIndex])
|
||||
}
|
||||
}
|
||||
48
ngtemplates/components/buttom.templ
Normal file
48
ngtemplates/components/buttom.templ
Normal file
@@ -0,0 +1,48 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ButtonVariant string
|
||||
|
||||
const (
|
||||
ButtonVariantPrimary ButtonVariant = "PRIMARY"
|
||||
ButtonVariantSecondary ButtonVariant = "SECONDAY"
|
||||
|
||||
baseClass string = "transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white"
|
||||
)
|
||||
|
||||
func (v ButtonVariant) getClass() string {
|
||||
variantClass, ok := variantClassMap[v]
|
||||
if !ok {
|
||||
variantClass = variantClassMap[ButtonVariantPrimary]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", variantClass, baseClass)
|
||||
}
|
||||
|
||||
var variantClassMap = map[ButtonVariant]string{
|
||||
ButtonVariantPrimary: "bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100",
|
||||
ButtonVariantSecondary: "bg-black shadow-md hover:text-black hover:bg-white",
|
||||
}
|
||||
|
||||
templ ButtonForm(title, formName string, variant ButtonVariant) {
|
||||
<button
|
||||
class={ variant.getClass() }
|
||||
type="submit"
|
||||
if formName != "" {
|
||||
form={ formName }
|
||||
}
|
||||
>
|
||||
{ title }
|
||||
</button>
|
||||
}
|
||||
|
||||
templ Button(title string, variant ButtonVariant) {
|
||||
@ButtonForm(title, "", variant)
|
||||
}
|
||||
|
||||
templ ButtonLink(title, url string, variant ButtonVariant) {
|
||||
<a href={ templ.SafeURL(url) } class={ "text-center", variant.getClass() } type="submit">{ title }</a>
|
||||
}
|
||||
219
ngtemplates/components/buttom_templ.go
Normal file
219
ngtemplates/components/buttom_templ.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ButtonVariant string
|
||||
|
||||
const (
|
||||
ButtonVariantPrimary ButtonVariant = "PRIMARY"
|
||||
ButtonVariantSecondary ButtonVariant = "SECONDAY"
|
||||
|
||||
baseClass string = "transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white"
|
||||
)
|
||||
|
||||
func (v ButtonVariant) getClass() string {
|
||||
variantClass, ok := variantClassMap[v]
|
||||
if !ok {
|
||||
variantClass = variantClassMap[ButtonVariantPrimary]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", variantClass, baseClass)
|
||||
}
|
||||
|
||||
var variantClassMap = map[ButtonVariant]string{
|
||||
ButtonVariantPrimary: "bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100",
|
||||
ButtonVariantSecondary: "bg-black shadow-md hover:text-black hover:bg-white",
|
||||
}
|
||||
|
||||
func ButtonForm(title, formName string, variant ButtonVariant) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var2 = []any{variant.getClass()}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<button class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/buttom.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" type=\"submit\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if formName != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " form=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(formName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/buttom.templ`, Line: 35, Col: 18}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/buttom.templ`, Line: 38, Col: 9}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Button(title string, variant ButtonVariant) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var6 == nil {
|
||||
templ_7745c5c3_Var6 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = ButtonForm(title, "", variant).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ButtonLink(title, url string, variant ButtonVariant) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var8 = []any{"text-center", variant.getClass()}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 templ.SafeURL = templ.SafeURL(url)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var9)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/buttom.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" type=\"submit\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/buttom.templ`, Line: 47, Col: 97}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
47
ngtemplates/components/daily_read_card.templ
Normal file
47
ngtemplates/components/daily_read_card.templ
Normal file
@@ -0,0 +1,47 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/graph"
|
||||
)
|
||||
|
||||
templ DailyReadChart(dailyReadSVG graph.SVGGraphData) {
|
||||
<div class="relative w-full bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<p class="absolute top-3 left-5 text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||||
Daily Read Totals
|
||||
</p>
|
||||
<div class="relative">
|
||||
<svg viewBox={ fmt.Sprintf("26 0 755 %d", dailyReadSVG.Height) } preserveAspectRatio="none" width="100%" height="6em">
|
||||
<!-- Bezier Line Graph -->
|
||||
<path
|
||||
fill="#316BBE"
|
||||
fill-opacity="0.5"
|
||||
stroke="none"
|
||||
d={ fmt.Sprintf("%s %s", dailyReadSVG.BezierPath, dailyReadSVG.BezierFill) }
|
||||
></path>
|
||||
<path fill="none" stroke="#316BBE" d={ dailyReadSVG.BezierPath }></path>
|
||||
</svg>
|
||||
<div
|
||||
class="flex absolute w-full h-full top-0"
|
||||
style="width: calc(100%*31/30); transform: translateX(-50%); left: 50%"
|
||||
>
|
||||
<!-- Required for iOS "Hover" Events (onclick) -->
|
||||
for _, item := range dailyReadSVG.LinePoints {
|
||||
<div
|
||||
onclick
|
||||
class="opacity-0 hover:opacity-100 w-full"
|
||||
style="background: linear-gradient(rgba(128, 128, 128, 0.5), rgba(128, 128, 128, 0.5)) no-repeat center/2px 100%"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-center p-2 rounded absolute top-3 dark:text-white text-xs pointer-events-none"
|
||||
style="transform: translateX(-50%); background-color: rgba(128, 128, 128, 0.2); left: 50%"
|
||||
>
|
||||
<span>{ item.Data.Label }</span>
|
||||
<span>{ fmt.Sprint(item.Data.Value) } minutes</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
120
ngtemplates/components/daily_read_card_templ.go
Normal file
120
ngtemplates/components/daily_read_card_templ.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/graph"
|
||||
)
|
||||
|
||||
func DailyReadChart(dailyReadSVG graph.SVGGraphData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"relative w-full bg-white shadow-lg dark:bg-gray-700 rounded\"><p class=\"absolute top-3 left-5 text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500\">Daily Read Totals</p><div class=\"relative\"><svg viewBox=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("26 0 755 %d", dailyReadSVG.Height))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/daily_read_card.templ`, Line: 14, Col: 65}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" preserveAspectRatio=\"none\" width=\"100%\" height=\"6em\"><!-- Bezier Line Graph --><path fill=\"#316BBE\" fill-opacity=\"0.5\" stroke=\"none\" d=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s %s", dailyReadSVG.BezierPath, dailyReadSVG.BezierFill))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/daily_read_card.templ`, Line: 20, Col: 79}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"></path> <path fill=\"none\" stroke=\"#316BBE\" d=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(dailyReadSVG.BezierPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/daily_read_card.templ`, Line: 22, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"></path></svg><div class=\"flex absolute w-full h-full top-0\" style=\"width: calc(100%*31/30); transform: translateX(-50%); left: 50%\"><!-- Required for iOS \"Hover\" Events (onclick) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range dailyReadSVG.LinePoints {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div onclick class=\"opacity-0 hover:opacity-100 w-full\" style=\"background: linear-gradient(rgba(128, 128, 128, 0.5), rgba(128, 128, 128, 0.5)) no-repeat center/2px 100%\"><div class=\"flex flex-col items-center p-2 rounded absolute top-3 dark:text-white text-xs pointer-events-none\" style=\"transform: translateX(-50%); background-color: rgba(128, 128, 128, 0.2); left: 50%\"><span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.Data.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/daily_read_card.templ`, Line: 39, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(item.Data.Value))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/daily_read_card.templ`, Line: 40, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " minutes</span></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
81
ngtemplates/components/document_card.templ
Normal file
81
ngtemplates/components/document_card.templ
Normal file
@@ -0,0 +1,81 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
)
|
||||
|
||||
func FirstNonZero[T comparable](all ...T) T {
|
||||
var zeroT T
|
||||
for _, item := range all {
|
||||
if item != zeroT {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return zeroT
|
||||
}
|
||||
|
||||
func orUnknown(val *string) string {
|
||||
if val == nil {
|
||||
return "Unknown"
|
||||
}
|
||||
return *val
|
||||
}
|
||||
|
||||
templ DocumentCard(document database.GetDocumentsWithStatsRow) {
|
||||
<div class="w-full relative">
|
||||
<div
|
||||
class="flex gap-4 w-full h-full p-4 shadow-lg bg-white dark:bg-gray-700 rounded"
|
||||
>
|
||||
<div class="min-w-fit my-auto h-48 relative">
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("./documents/%s", document.ID)) }>
|
||||
<img
|
||||
class="rounded object-cover h-full"
|
||||
src={ fmt.Sprintf("./documents/%s/cover", document.ID) }
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Title</p>
|
||||
<p class="font-medium">{ orUnknown(document.Title) }</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Author</p>
|
||||
<p class="font-medium">{ orUnknown(document.Author) }</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Progress</p>
|
||||
<p class="font-medium">{ fmt.Sprintf("%.2f%%", document.Percentage) }</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Time Read</p>
|
||||
<p class="font-medium">{ common.NiceSeconds(document.TotalTimeSeconds) }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<a href="./activity?document={{ .ID }}">
|
||||
@ActivitySVG("")
|
||||
</a>
|
||||
if document.Filepath != nil && *document.Filepath != "" {
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("./documents/%s/file", document.ID)) }>
|
||||
@DownloadSVG("")
|
||||
</a>
|
||||
} else {
|
||||
@DownloadSVG("text-gray-200 dark:text-gray-600")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
177
ngtemplates/components/document_card_templ.go
Normal file
177
ngtemplates/components/document_card_templ.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
)
|
||||
|
||||
func FirstNonZero[T comparable](all ...T) T {
|
||||
var zeroT T
|
||||
for _, item := range all {
|
||||
if item != zeroT {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return zeroT
|
||||
}
|
||||
|
||||
func orUnknown(val *string) string {
|
||||
if val == nil {
|
||||
return "Unknown"
|
||||
}
|
||||
return *val
|
||||
}
|
||||
|
||||
func DocumentCard(document database.GetDocumentsWithStatsRow) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full relative\"><div class=\"flex gap-4 w-full h-full p-4 shadow-lg bg-white dark:bg-gray-700 rounded\"><div class=\"min-w-fit my-auto h-48 relative\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL = templ.SafeURL(fmt.Sprintf("./documents/%s", document.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><img class=\"rounded object-cover h-full\" src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("./documents/%s/cover", document.ID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/document_card.templ`, Line: 35, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"></a></div><div class=\"flex flex-col justify-around dark:text-white w-full text-sm\"><div class=\"inline-flex shrink-0 items-center\"><div><p class=\"text-gray-400\">Title</p><p class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(orUnknown(document.Title))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/document_card.templ`, Line: 43, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p></div></div><div class=\"inline-flex shrink-0 items-center\"><div><p class=\"text-gray-400\">Author</p><p class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(orUnknown(document.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/document_card.templ`, Line: 49, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div></div><div class=\"inline-flex shrink-0 items-center\"><div><p class=\"text-gray-400\">Progress</p><p class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f%%", document.Percentage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/document_card.templ`, Line: 55, Col: 73}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</p></div></div><div class=\"inline-flex shrink-0 items-center\"><div><p class=\"text-gray-400\">Time Read</p><p class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(common.NiceSeconds(document.TotalTimeSeconds))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/document_card.templ`, Line: 61, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p></div></div></div><div class=\"absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400\"><a href=\"./activity?document={{ .ID }}\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = ActivitySVG("").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if document.Filepath != nil && *document.Filepath != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 templ.SafeURL = templ.SafeURL(fmt.Sprintf("./documents/%s/file", document.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var8)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = DownloadSVG("").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = DownloadSVG("text-gray-200 dark:text-gray-600").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
26
ngtemplates/components/info_card.templ
Normal file
26
ngtemplates/components/info_card.templ
Normal file
@@ -0,0 +1,26 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
templ InfoCard(name string, metric int) {
|
||||
<div class="w-full">
|
||||
@infoCardInner(name, metric)
|
||||
</div>
|
||||
}
|
||||
|
||||
templ InfoCardLink(name string, metric int, link string) {
|
||||
<a href={ templ.SafeURL(link) } class="w-full">
|
||||
@infoCardInner(name, metric)
|
||||
</a>
|
||||
}
|
||||
|
||||
templ infoCardInner(name string, metric int) {
|
||||
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<p class="text-2xl font-bold text-black dark:text-white">{ fmt.Sprint(metric) }</p>
|
||||
<p class="text-sm text-gray-400">{ name }</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
153
ngtemplates/components/info_card_templ.go
Normal file
153
ngtemplates/components/info_card_templ.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func InfoCard(name string, metric int) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = infoCardInner(name, metric).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func InfoCardLink(name string, metric int, link string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var2 == nil {
|
||||
templ_7745c5c3_Var2 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 templ.SafeURL = templ.SafeURL(link)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = infoCardInner(name, metric).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func infoCardInner(name string, metric int) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded\"><div class=\"flex flex-col justify-around dark:text-white w-full text-sm\"><p class=\"text-2xl font-bold text-black dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(metric))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/info_card.templ`, Line: 22, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p><p class=\"text-sm text-gray-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/info_card.templ`, Line: 23, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
91
ngtemplates/components/leaderboard_card.templ
Normal file
91
ngtemplates/components/leaderboard_card.templ
Normal file
@@ -0,0 +1,91 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
)
|
||||
|
||||
templ LeaderboardCard(name string, stats map[string][]common.UserStatisticEntry) {
|
||||
<div class="w-full">
|
||||
<div class="flex flex-col justify-between h-full w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<div>
|
||||
<div class="flex justify-between">
|
||||
<p class="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||||
{ name } Leaderboard
|
||||
</p>
|
||||
<div class="flex gap-2 text-xs text-gray-400 items-center">
|
||||
<label
|
||||
for={ fmt.Sprintf("all-%s", name) }
|
||||
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||||
>all</label>
|
||||
<label
|
||||
for={ fmt.Sprintf("year-%s", name) }
|
||||
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||||
>year</label>
|
||||
<label
|
||||
for={ fmt.Sprintf("month-%s", name) }
|
||||
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||||
>month</label>
|
||||
<label
|
||||
for={ fmt.Sprintf("week-%s", name) }
|
||||
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||||
>week</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="radio"
|
||||
name={ fmt.Sprintf("options-%s", name) }
|
||||
id={ fmt.Sprintf("all-%s", name) }
|
||||
class="hidden peer/All"
|
||||
checked
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name={ fmt.Sprintf("options-%s", name) }
|
||||
id={ fmt.Sprintf("year-%s", name) }
|
||||
class="hidden peer/Year"
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name={ fmt.Sprintf("options-%s", name) }
|
||||
id={ fmt.Sprintf("month-%s", name) }
|
||||
class="hidden peer/Month"
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name={ fmt.Sprintf("options-%s", name) }
|
||||
id={ fmt.Sprintf("week-%s", name) }
|
||||
class="hidden peer/Week"
|
||||
/>
|
||||
for name, data := range stats {
|
||||
<div class={ "flex items-end my-6 space-x-2 hidden", fmt.Sprintf("peer-checked/%s:block", name) }>
|
||||
if len(data) == 0 {
|
||||
<p class="text-5xl font-bold text-black dark:text-white">N/A</p>
|
||||
} else {
|
||||
<p class="text-5xl font-bold text-black dark:text-white">{ data[0].UserID }</p>
|
||||
}
|
||||
</div>
|
||||
<div class={ "hidden dark:text-white", fmt.Sprintf("peer-checked/%s:block", name) }>
|
||||
for idx, item := range data {
|
||||
if idx == 0 {
|
||||
<div class="flex items-center justify-between pt-2 pb-2 text-sm">
|
||||
<div>
|
||||
<p>{ item.UserID }</p>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{ item.Value }</div>
|
||||
</div>
|
||||
} else if idx < 3 {
|
||||
<div class="flex items-center justify-between pt-2 pb-2 text-sm border-t border-gray-200">
|
||||
<div>
|
||||
<p>{ item.UserID }</p>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{ item.Value }</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
361
ngtemplates/components/leaderboard_card_templ.go
Normal file
361
ngtemplates/components/leaderboard_card_templ.go
Normal file
@@ -0,0 +1,361 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
)
|
||||
|
||||
func LeaderboardCard(name string, stats map[string][]common.UserStatisticEntry) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full\"><div class=\"flex flex-col justify-between h-full w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded\"><div><div class=\"flex justify-between\"><p class=\"text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 14, Col: 12}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " Leaderboard</p><div class=\"flex gap-2 text-xs text-gray-400 items-center\"><label for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("all-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 18, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"cursor-pointer hover:text-black dark:hover:text-white\">all</label> <label for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("year-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 22, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"cursor-pointer hover:text-black dark:hover:text-white\">year</label> <label for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("month-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 26, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"cursor-pointer hover:text-black dark:hover:text-white\">month</label> <label for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("week-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 30, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"cursor-pointer hover:text-black dark:hover:text-white\">week</label></div></div></div><input type=\"radio\" name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("options-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 38, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("all-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 39, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"hidden peer/All\" checked> <input type=\"radio\" name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("options-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 45, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("year-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 46, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"hidden peer/Year\"> <input type=\"radio\" name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("options-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 51, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("month-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 52, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" class=\"hidden peer/Month\"> <input type=\"radio\" name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("options-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 57, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("week-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 58, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"hidden peer/Week\"> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for name, data := range stats {
|
||||
var templ_7745c5c3_Var15 = []any{"flex items-end my-6 space-x-2 hidden", fmt.Sprintf("peer-checked/%s:block", name)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<p class=\"text-5xl font-bold text-black dark:text-white\">N/A</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<p class=\"text-5xl font-bold text-black dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data[0].UserID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 66, Col: 79}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 = []any{"hidden dark:text-white", fmt.Sprintf("peer-checked/%s:block", name)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var18).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for idx, item := range data {
|
||||
if idx == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"flex items-center justify-between pt-2 pb-2 text-sm\"><div><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(item.UserID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 74, Col: 25}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p></div><div class=\"flex items-end font-bold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(item.Value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 76, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if idx < 3 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div class=\"flex items-center justify-between pt-2 pb-2 text-sm border-t border-gray-200\"><div><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(item.UserID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 81, Col: 25}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</p></div><div class=\"flex items-end font-bold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(item.Value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 83, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
57
ngtemplates/components/streak_card.templ
Normal file
57
ngtemplates/components/streak_card.templ
Normal file
@@ -0,0 +1,57 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
)
|
||||
|
||||
templ StreakCard(streak database.UserStreak) {
|
||||
<div class="w-full">
|
||||
<div class="relative w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<p class="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||||
if streak.Window == "WEEK" {
|
||||
Weekly Read Streak
|
||||
} else {
|
||||
Daily Read Streak
|
||||
}
|
||||
</p>
|
||||
<div class="flex items-end my-6 space-x-2">
|
||||
<p class="text-5xl font-bold text-black dark:text-white">
|
||||
{ fmt.Sprint(streak.CurrentStreak) }
|
||||
</p>
|
||||
</div>
|
||||
<div class="dark:text-white">
|
||||
<div class="flex items-center justify-between pb-2 mb-2 text-sm border-b border-gray-200">
|
||||
<div>
|
||||
<p>
|
||||
if streak.Window == "WEEK" {
|
||||
Current Read Streak
|
||||
} else {
|
||||
Current Read Streak
|
||||
}
|
||||
</p>
|
||||
<div class="flex items-end text-sm text-gray-400">
|
||||
{ streak.CurrentStreakStartDate } ➞ { streak.CurrentStreakEndDate }
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{ fmt.Sprint(streak.CurrentStreak) }</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between pb-2 mb-2 text-sm">
|
||||
<div>
|
||||
<p>
|
||||
if streak.Window == "WEEK" {
|
||||
Best Weekly Streak
|
||||
} else {
|
||||
Best Daily Streak
|
||||
}
|
||||
</p>
|
||||
<div class="flex items-end text-sm text-gray-400">
|
||||
{ streak.MaxStreakStartDate } ➞ { streak.MaxStreakEndDate }
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{ fmt.Sprint(streak.MaxStreak) }</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
181
ngtemplates/components/streak_card_templ.go
Normal file
181
ngtemplates/components/streak_card_templ.go
Normal file
@@ -0,0 +1,181 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
)
|
||||
|
||||
func StreakCard(streak database.UserStreak) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full\"><div class=\"relative w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded\"><p class=\"text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if streak.Window == "WEEK" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "Weekly Read Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Daily Read Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p><div class=\"flex items-end my-6 space-x-2\"><p class=\"text-5xl font-bold text-black dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(streak.CurrentStreak))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 20, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div><div class=\"dark:text-white\"><div class=\"flex items-center justify-between pb-2 mb-2 text-sm border-b border-gray-200\"><div><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if streak.Window == "WEEK" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Current Read Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "Current Read Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p><div class=\"flex items-end text-sm text-gray-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(streak.CurrentStreakStartDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 34, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ➞ ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(streak.CurrentStreakEndDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 34, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></div><div class=\"flex items-end font-bold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(streak.CurrentStreak))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 37, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div><div class=\"flex items-center justify-between pb-2 mb-2 text-sm\"><div><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if streak.Window == "WEEK" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Best Weekly Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Best Daily Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p><div class=\"flex items-end text-sm text-gray-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(streak.MaxStreakStartDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 49, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ➞ ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(streak.MaxStreakEndDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 49, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div></div><div class=\"flex items-end font-bold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(streak.MaxStreak))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 52, Col: 73}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div></div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
342
ngtemplates/components/svgs.templ
Normal file
342
ngtemplates/components/svgs.templ
Normal file
@@ -0,0 +1,342 @@
|
||||
package components
|
||||
|
||||
templ ActivitySVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M9.5 2C8.67157 2 8 2.67157 8 3.5V4.5C8 5.32843 8.67157 6 9.5 6H14.5C15.3284 6 16 5.32843 16 4.5V3.5C16 2.67157 15.3284 2 14.5 2H9.5Z"></path>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 4.03662C5.24209 4.10719 4.44798 4.30764 3.87868 4.87694C3 5.75562 3 7.16983 3 9.99826V15.9983C3 18.8267 3 20.2409 3.87868 21.1196C4.75736 21.9983 6.17157 21.9983 9 21.9983H15C17.8284 21.9983 19.2426 21.9983 20.1213 21.1196C21 20.2409 21 18.8267 21 15.9983V9.99826C21 7.16983 21 5.75562 20.1213 4.87694C19.552 4.30764 18.7579 4.10719 17.5 4.03662V4.5C17.5 6.15685 16.1569 7.5 14.5 7.5H9.5C7.84315 7.5 6.5 6.15685 6.5 4.5V4.03662ZM7 9.75C6.58579 9.75 6.25 10.0858 6.25 10.5C6.25 10.9142 6.58579 11.25 7 11.25H7.5C7.91421 11.25 8.25 10.9142 8.25 10.5C8.25 10.0858 7.91421 9.75 7.5 9.75H7ZM10.5 9.75C10.0858 9.75 9.75 10.0858 9.75 10.5C9.75 10.9142 10.0858 11.25 10.5 11.25H17C17.4142 11.25 17.75 10.9142 17.75 10.5C17.75 10.0858 17.4142 9.75 17 9.75H10.5ZM7 13.25C6.58579 13.25 6.25 13.5858 6.25 14C6.25 14.4142 6.58579 14.75 7 14.75H7.5C7.91421 14.75 8.25 14.4142 8.25 14C8.25 13.5858 7.91421 13.25 7.5 13.25H7ZM10.5 13.25C10.0858 13.25 9.75 13.5858 9.75 14C9.75 14.4142 10.0858 14.75 10.5 14.75H17C17.4142 14.75 17.75 14.4142 17.75 14C17.75 13.5858 17.4142 13.25 17 13.25H10.5ZM7 16.75C6.58579 16.75 6.25 17.0858 6.25 17.5C6.25 17.9142 6.58579 18.25 7 18.25H7.5C7.91421 18.25 8.25 17.9142 8.25 17.5C8.25 17.0858 7.91421 16.75 7.5 16.75H7ZM10.5 16.75C10.0858 16.75 9.75 17.0858 9.75 17.5C9.75 17.9142 10.0858 18.25 10.5 18.25H17C17.4142 18.25 17.75 17.9142 17.75 17.5C17.75 17.0858 17.4142 16.75 17 16.75H10.5Z"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ AddSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 8.25C12.4142 8.25 12.75 8.58579 12.75 9V11.25H15C15.4142 11.25 15.75 11.5858 15.75 12C15.75 12.4142 15.4142 12.75 15 12.75H12.75L12.75 15C12.75 15.4142 12.4142 15.75 12 15.75C11.5858 15.75 11.25 15.4142 11.25 15V12.75H9C8.58579 12.75 8.25 12.4142 8.25 12C8.25 11.5858 8.58579 11.25 9 11.25H11.25L11.25 9C11.25 8.58579 11.5858 8.25 12 8.25Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ ClockSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
|
||||
></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 7.25C12.4142 7.25 12.75 7.58579 12.75 8V11.6893L15.0303 13.9697C15.3232 14.2626 15.3232 14.7374 15.0303 15.0303C14.7374 15.3232 14.2626 15.3232 13.9697 15.0303L11.4697 12.5303C11.329 12.3897 11.25 12.1989 11.25 12V8C11.25 7.58579 11.5858 7.25 12 7.25Z"
|
||||
fill="white"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ DeleteSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M3 6.52381C3 6.12932 3.32671 5.80952 3.72973 5.80952H8.51787C8.52437 4.9683 8.61554 3.81504 9.45037 3.01668C10.1074 2.38839 11.0081 2 12 2C12.9919 2 13.8926 2.38839 14.5496 3.01668C15.3844 3.81504 15.4756 4.9683 15.4821 5.80952H20.2703C20.6733 5.80952 21 6.12932 21 6.52381C21 6.9183 20.6733 7.2381 20.2703 7.2381H3.72973C3.32671 7.2381 3 6.9183 3 6.52381Z"
|
||||
></path>
|
||||
<path
|
||||
d="M11.6066 22H12.3935C15.101 22 16.4547 22 17.3349 21.1368C18.2151 20.2736 18.3052 18.8576 18.4853 16.0257L18.7448 11.9452C18.8425 10.4086 18.8913 9.64037 18.4498 9.15352C18.0082 8.66667 17.2625 8.66667 15.7712 8.66667H8.22884C6.7375 8.66667 5.99183 8.66667 5.55026 9.15352C5.1087 9.64037 5.15756 10.4086 5.25528 11.9452L5.51479 16.0257C5.69489 18.8576 5.78494 20.2736 6.66513 21.1368C7.54532 22 8.89906 22 11.6066 22Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ DocumentSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.27103 2.11151C5.46135 2.21816 5.03258 2.41324 4.72718 2.71244C4.42179 3.01165 4.22268 3.43172 4.11382 4.225C4.00176 5.04159 4 6.12387 4 7.67568V16.2442C4.38867 15.9781 4.82674 15.7756 5.29899 15.6517C5.82716 15.513 6.44305 15.5132 7.34563 15.5135L20 15.5135V7.67568C20 6.12387 19.9982 5.04159 19.8862 4.22499C19.7773 3.43172 19.5782 3.01165 19.2728 2.71244C18.9674 2.41324 18.5387 2.21816 17.729 2.11151C16.8955 2.00172 15.7908 2 14.2069 2H9.7931C8.2092 2 7.10452 2.00172 6.27103 2.11151ZM6.75862 6.59459C6.75862 6.1468 7.12914 5.78378 7.58621 5.78378H16.4138C16.8709 5.78378 17.2414 6.1468 17.2414 6.59459C17.2414 7.04239 16.8709 7.40541 16.4138 7.40541H7.58621C7.12914 7.40541 6.75862 7.04239 6.75862 6.59459ZM7.58621 9.56757C7.12914 9.56757 6.75862 9.93058 6.75862 10.3784C6.75862 10.8262 7.12914 11.1892 7.58621 11.1892H13.1034C13.5605 11.1892 13.931 10.8262 13.931 10.3784C13.931 9.93058 13.5605 9.56757 13.1034 9.56757H7.58621Z"></path>
|
||||
<path d="M7.47341 17.1351H8.68965H13.1034H19.9991C19.9956 18.2657 19.9776 19.1088 19.8862 19.775C19.7773 20.5683 19.5782 20.9884 19.2728 21.2876C18.9674 21.5868 18.5387 21.7818 17.729 21.8885C16.8955 21.9983 15.7908 22 14.2069 22H9.7931C8.2092 22 7.10452 21.9983 6.27103 21.8885C5.46135 21.7818 5.03258 21.5868 4.72718 21.2876C4.42179 20.9884 4.22268 20.5683 4.11382 19.775C4.07259 19.4746 4.0463 19.1382 4.02952 18.7558C4.30088 18.0044 4.93365 17.4264 5.72738 17.218C6.01657 17.1421 6.39395 17.1351 7.47341 17.1351Z"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ DownloadSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25ZM8 16.25C7.58579 16.25 7.25 16.5858 7.25 17C7.25 17.4142 7.58579 17.75 8 17.75H16C16.4142 17.75 16.75 17.4142 16.75 17C16.75 16.5858 16.4142 16.25 16 16.25H8Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ DropdownSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
viewBox="0 0 1792 1792"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M1408 704q0 26-19 45l-448 448q-19 19-45 19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ EditSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
|
||||
></path>
|
||||
<path
|
||||
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
|
||||
></path>
|
||||
<path
|
||||
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ GitSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 219 92"
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path d="M159 .79h25V69h-25Zm0 0"></path>
|
||||
</clipPath>
|
||||
<clipPath id="b">
|
||||
<path d="M183 9h35.371v60H183Zm0 0"></path>
|
||||
</clipPath>
|
||||
<clipPath id="c">
|
||||
<path d="M0 .79h92V92H0Zm0 0"></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61"
|
||||
></path>
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805"
|
||||
></path>
|
||||
</g>
|
||||
<g clip-path="url(#b)">
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66"
|
||||
></path>
|
||||
</g>
|
||||
<g clip-path="url(#c)">
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ HomeSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5192 7.82274C2 8.77128 2 9.91549 2 12.2039V13.725C2 17.6258 2 19.5763 3.17157 20.7881C4.34315 22 6.22876 22 10 22H14C17.7712 22 19.6569 22 20.8284 20.7881C22 19.5763 22 17.6258 22 13.725V12.2039C22 9.91549 22 8.77128 21.4808 7.82274C20.9616 6.87421 20.0131 6.28551 18.116 5.10812L16.116 3.86687C14.1106 2.62229 13.1079 2 12 2C10.8921 2 9.88939 2.62229 7.88403 3.86687L5.88403 5.10813C3.98695 6.28551 3.0384 6.87421 2.5192 7.82274ZM11.25 18C11.25 18.4142 11.5858 18.75 12 18.75C12.4142 18.75 12.75 18.4142 12.75 18V15C12.75 14.5858 12.4142 14.25 12 14.25C11.5858 14.25 11.25 14.5858 11.25 15V18Z"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ ImportSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.06935 5.00839C2 5.37595 2 5.81722 2 6.69975V13.75C2 17.5212 2 19.4069 3.17157 20.5784C4.34315 21.75 6.22876 21.75 10 21.75H14C17.7712 21.75 19.6569 21.75 20.8284 20.5784C22 19.4069 22 17.5212 22 13.75V11.5479C22 8.91554 22 7.59935 21.2305 6.74383C21.1598 6.66514 21.0849 6.59024 21.0062 6.51946C20.1506 5.75 18.8345 5.75 16.2021 5.75H15.8284C14.6747 5.75 14.0979 5.75 13.5604 5.59678C13.2651 5.5126 12.9804 5.39471 12.7121 5.24543C12.2237 4.97367 11.8158 4.56578 11 3.75L10.4497 3.19975C10.1763 2.92633 10.0396 2.78961 9.89594 2.67051C9.27652 2.15704 8.51665 1.84229 7.71557 1.76738C7.52976 1.75 7.33642 1.75 6.94975 1.75C6.06722 1.75 5.62595 1.75 5.25839 1.81935C3.64031 2.12464 2.37464 3.39031 2.06935 5.00839ZM12 11C12.4142 11 12.75 11.3358 12.75 11.75V13H14C14.4142 13 14.75 13.3358 14.75 13.75C14.75 14.1642 14.4142 14.5 14 14.5H12.75V15.75C12.75 16.1642 12.4142 16.5 12 16.5C11.5858 16.5 11.25 16.1642 11.25 15.75V14.5H10C9.58579 14.5 9.25 14.1642 9.25 13.75C9.25 13.3358 9.58579 13 10 13H11.25V11.75C11.25 11.3358 11.5858 11 12 11Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ InfoSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 17.75C12.4142 17.75 12.75 17.4142 12.75 17V11C12.75 10.5858 12.4142 10.25 12 10.25C11.5858 10.25 11.25 10.5858 11.25 11V17C11.25 17.4142 11.5858 17.75 12 17.75ZM12 7C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ LoadingSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<style>
|
||||
.spinner_l9ve {
|
||||
animation: spinner_rcyq 1.2s cubic-bezier(0.52, 0.6, 0.25, 0.99) infinite;
|
||||
}
|
||||
.spinner_cMYp {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.spinner_gHR3 {
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
@keyframes spinner_rcyq {
|
||||
0% {
|
||||
transform: translate(12px, 12px) scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
class="spinner_l9ve"
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z"
|
||||
transform="translate(12, 12) scale(0)"
|
||||
></path>
|
||||
<path
|
||||
class="spinner_l9ve spinner_cMYp"
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z"
|
||||
transform="translate(12, 12) scale(0)"
|
||||
></path>
|
||||
<path
|
||||
class="spinner_l9ve spinner_gHR3"
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z"
|
||||
transform="translate(12, 12) scale(0)"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ PasswordSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 1792 1792"
|
||||
>
|
||||
<path
|
||||
d="M1376 768q40 0 68 28t28 68v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h32v-320q0-185 131.5-316.5t316.5-131.5 316.5 131.5 131.5 316.5q0 26-19 45t-45 19h-64q-26 0-45-19t-19-45q0-106-75-181t-181-75-181 75-75 181v320h736z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ SearchSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM9 11.5C9 10.1193 10.1193 9 11.5 9C12.8807 9 14 10.1193 14 11.5C14 12.8807 12.8807 14 11.5 14C10.1193 14 9 12.8807 9 11.5ZM11.5 7C9.01472 7 7 9.01472 7 11.5C7 13.9853 9.01472 16 11.5 16C12.3805 16 13.202 15.7471 13.8957 15.31L15.2929 16.7071C15.6834 17.0976 16.3166 17.0976 16.7071 16.7071C17.0976 16.3166 17.0976 15.6834 16.7071 15.2929L15.31 13.8957C15.7471 13.202 16 12.3805 16 11.5C16 9.01472 13.9853 7 11.5 7Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ Search2SVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<rect width="24" height="24" fill="none"></rect>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C11.8487 18 13.551 17.3729 14.9056 16.3199L20.2929 21.7071C20.6834 22.0976 21.3166 22.0976 21.7071 21.7071C22.0976 21.3166 22.0976 20.6834 21.7071 20.2929L16.3199 14.9056C17.3729 13.551 18 11.8487 18 10C18 5.58172 14.4183 2 10 2Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ SettingsSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.2788 2.15224C13.9085 2 13.439 2 12.5 2C11.561 2 11.0915 2 10.7212 2.15224C10.2274 2.35523 9.83509 2.74458 9.63056 3.23463C9.53719 3.45834 9.50065 3.7185 9.48635 4.09799C9.46534 4.65568 9.17716 5.17189 8.69017 5.45093C8.20318 5.72996 7.60864 5.71954 7.11149 5.45876C6.77318 5.2813 6.52789 5.18262 6.28599 5.15102C5.75609 5.08178 5.22018 5.22429 4.79616 5.5472C4.47814 5.78938 4.24339 6.1929 3.7739 6.99993C3.30441 7.80697 3.06967 8.21048 3.01735 8.60491C2.94758 9.1308 3.09118 9.66266 3.41655 10.0835C3.56506 10.2756 3.77377 10.437 4.0977 10.639C4.57391 10.936 4.88032 11.4419 4.88029 12C4.88026 12.5581 4.57386 13.0639 4.0977 13.3608C3.77372 13.5629 3.56497 13.7244 3.41645 13.9165C3.09108 14.3373 2.94749 14.8691 3.01725 15.395C3.06957 15.7894 3.30432 16.193 3.7738 17C4.24329 17.807 4.47804 18.2106 4.79606 18.4527C5.22008 18.7756 5.75599 18.9181 6.28589 18.8489C6.52778 18.8173 6.77305 18.7186 7.11133 18.5412C7.60852 18.2804 8.2031 18.27 8.69012 18.549C9.17714 18.8281 9.46533 19.3443 9.48635 19.9021C9.50065 20.2815 9.53719 20.5417 9.63056 20.7654C9.83509 21.2554 10.2274 21.6448 10.7212 21.8478C11.0915 22 11.561 22 12.5 22C13.439 22 13.9085 22 14.2788 21.8478C14.7726 21.6448 15.1649 21.2554 15.3694 20.7654C15.4628 20.5417 15.4994 20.2815 15.5137 19.902C15.5347 19.3443 15.8228 18.8281 16.3098 18.549C16.7968 18.2699 17.3914 18.2804 17.8886 18.5412C18.2269 18.7186 18.4721 18.8172 18.714 18.8488C19.2439 18.9181 19.7798 18.7756 20.2038 18.4527C20.5219 18.2105 20.7566 17.807 21.2261 16.9999C21.6956 16.1929 21.9303 15.7894 21.9827 15.395C22.0524 14.8691 21.9088 14.3372 21.5835 13.9164C21.4349 13.7243 21.2262 13.5628 20.9022 13.3608C20.4261 13.0639 20.1197 12.558 20.1197 11.9999C20.1197 11.4418 20.4261 10.9361 20.9022 10.6392C21.2263 10.4371 21.435 10.2757 21.5836 10.0835C21.9089 9.66273 22.0525 9.13087 21.9828 8.60497C21.9304 8.21055 21.6957 7.80703 21.2262 7C20.7567 6.19297 20.522 5.78945 20.2039 5.54727C19.7799 5.22436 19.244 5.08185 18.7141 5.15109C18.4722 5.18269 18.2269 5.28136 17.8887 5.4588C17.3915 5.71959 16.7969 5.73002 16.3099 5.45096C15.8229 5.17191 15.5347 4.65566 15.5136 4.09794C15.4993 3.71848 15.4628 3.45833 15.3694 3.23463C15.1649 2.74458 14.7726 2.35523 14.2788 2.15224ZM12.5 15C14.1695 15 15.5228 13.6569 15.5228 12C15.5228 10.3431 14.1695 9 12.5 9C10.8305 9 9.47716 10.3431 9.47716 12C9.47716 13.6569 10.8305 15 12.5 15Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ UploadSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 15.75C12.4142 15.75 12.75 15.4142 12.75 15V4.02744L14.4306 5.98809C14.7001 6.30259 15.1736 6.33901 15.4881 6.06944C15.8026 5.79988 15.839 5.3264 15.5694 5.01191L12.5694 1.51191C12.427 1.34567 12.2189 1.25 12 1.25C11.7811 1.25 11.573 1.34567 11.4306 1.51191L8.43056 5.01191C8.16099 5.3264 8.19741 5.79988 8.51191 6.06944C8.8264 6.33901 9.29988 6.30259 9.56944 5.98809L11.25 4.02744L11.25 15C11.25 15.4142 11.5858 15.75 12 15.75Z"
|
||||
></path>
|
||||
<path
|
||||
d="M16 9C15.2978 9 14.9467 9 14.6945 9.16851C14.5853 9.24148 14.4915 9.33525 14.4186 9.44446C14.25 9.69667 14.25 10.0478 14.25 10.75L14.25 15C14.25 16.2426 13.2427 17.25 12 17.25C10.7574 17.25 9.75004 16.2426 9.75004 15L9.75004 10.75C9.75004 10.0478 9.75004 9.69664 9.58149 9.4444C9.50854 9.33523 9.41481 9.2415 9.30564 9.16855C9.05341 9 8.70227 9 8 9C5.17157 9 3.75736 9 2.87868 9.87868C2 10.7574 2 12.1714 2 14.9998V15.9998C2 18.8282 2 20.2424 2.87868 21.1211C3.75736 21.9998 5.17157 21.9998 8 21.9998H16C18.8284 21.9998 20.2426 21.9998 21.1213 21.1211C22 20.2424 22 18.8282 22 15.9998V14.9998C22 12.1714 22 10.7574 21.1213 9.87868C20.2426 9 18.8284 9 16 9Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ UserSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 1792 1792"
|
||||
>
|
||||
<path
|
||||
d="M1523 1339q-22-155-87.5-257.5t-184.5-118.5q-67 74-159.5 115.5t-195.5 41.5-195.5-41.5-159.5-115.5q-119 16-184.5 118.5t-87.5 257.5q106 150 271 237.5t356 87.5 356-87.5 271-237.5zm-243-699q0-159-112.5-271.5t-271.5-112.5-271.5 112.5-112.5 271.5 112.5 271.5 271.5 112.5 271.5-112.5 112.5-271.5zm512 256q0 182-71 347.5t-190.5 286-285.5 191.5-349 71q-182 0-348-71t-286-191-191-286-71-348 71-348 191-286 286-191 348-71 348 71 286 191 191 286 71 348z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
904
ngtemplates/components/svgs_templ.go
Normal file
904
ngtemplates/components/svgs_templ.go
Normal file
@@ -0,0 +1,904 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func ActivitySVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var2 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M9.5 2C8.67157 2 8 2.67157 8 3.5V4.5C8 5.32843 8.67157 6 9.5 6H14.5C15.3284 6 16 5.32843 16 4.5V3.5C16 2.67157 15.3284 2 14.5 2H9.5Z\"></path> <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M6.5 4.03662C5.24209 4.10719 4.44798 4.30764 3.87868 4.87694C3 5.75562 3 7.16983 3 9.99826V15.9983C3 18.8267 3 20.2409 3.87868 21.1196C4.75736 21.9983 6.17157 21.9983 9 21.9983H15C17.8284 21.9983 19.2426 21.9983 20.1213 21.1196C21 20.2409 21 18.8267 21 15.9983V9.99826C21 7.16983 21 5.75562 20.1213 4.87694C19.552 4.30764 18.7579 4.10719 17.5 4.03662V4.5C17.5 6.15685 16.1569 7.5 14.5 7.5H9.5C7.84315 7.5 6.5 6.15685 6.5 4.5V4.03662ZM7 9.75C6.58579 9.75 6.25 10.0858 6.25 10.5C6.25 10.9142 6.58579 11.25 7 11.25H7.5C7.91421 11.25 8.25 10.9142 8.25 10.5C8.25 10.0858 7.91421 9.75 7.5 9.75H7ZM10.5 9.75C10.0858 9.75 9.75 10.0858 9.75 10.5C9.75 10.9142 10.0858 11.25 10.5 11.25H17C17.4142 11.25 17.75 10.9142 17.75 10.5C17.75 10.0858 17.4142 9.75 17 9.75H10.5ZM7 13.25C6.58579 13.25 6.25 13.5858 6.25 14C6.25 14.4142 6.58579 14.75 7 14.75H7.5C7.91421 14.75 8.25 14.4142 8.25 14C8.25 13.5858 7.91421 13.25 7.5 13.25H7ZM10.5 13.25C10.0858 13.25 9.75 13.5858 9.75 14C9.75 14.4142 10.0858 14.75 10.5 14.75H17C17.4142 14.75 17.75 14.4142 17.75 14C17.75 13.5858 17.4142 13.25 17 13.25H10.5ZM7 16.75C6.58579 16.75 6.25 17.0858 6.25 17.5C6.25 17.9142 6.58579 18.25 7 18.25H7.5C7.91421 18.25 8.25 17.9142 8.25 17.5C8.25 17.0858 7.91421 16.75 7.5 16.75H7ZM10.5 16.75C10.0858 16.75 9.75 17.0858 9.75 17.5C9.75 17.9142 10.0858 18.25 10.5 18.25H17C17.4142 18.25 17.75 17.9142 17.75 17.5C17.75 17.0858 17.4142 16.75 17 16.75H10.5Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func AddSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var5 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 8.25C12.4142 8.25 12.75 8.58579 12.75 9V11.25H15C15.4142 11.25 15.75 11.5858 15.75 12C15.75 12.4142 15.4142 12.75 15 12.75H12.75L12.75 15C12.75 15.4142 12.4142 15.75 12 15.75C11.5858 15.75 11.25 15.4142 11.25 15V12.75H9C8.58579 12.75 8.25 12.4142 8.25 12C8.25 11.5858 8.58579 11.25 9 11.25H11.25L11.25 9C11.25 8.58579 11.5858 8.25 12 8.25Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ClockSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var8 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z\"></path> <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.25C12.4142 7.25 12.75 7.58579 12.75 8V11.6893L15.0303 13.9697C15.3232 14.2626 15.3232 14.7374 15.0303 15.0303C14.7374 15.3232 14.2626 15.3232 13.9697 15.0303L11.4697 12.5303C11.329 12.3897 11.25 12.1989 11.25 12V8C11.25 7.58579 11.5858 7.25 12 7.25Z\" fill=\"white\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var10 == nil {
|
||||
templ_7745c5c3_Var10 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var11 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M3 6.52381C3 6.12932 3.32671 5.80952 3.72973 5.80952H8.51787C8.52437 4.9683 8.61554 3.81504 9.45037 3.01668C10.1074 2.38839 11.0081 2 12 2C12.9919 2 13.8926 2.38839 14.5496 3.01668C15.3844 3.81504 15.4756 4.9683 15.4821 5.80952H20.2703C20.6733 5.80952 21 6.12932 21 6.52381C21 6.9183 20.6733 7.2381 20.2703 7.2381H3.72973C3.32671 7.2381 3 6.9183 3 6.52381Z\"></path> <path d=\"M11.6066 22H12.3935C15.101 22 16.4547 22 17.3349 21.1368C18.2151 20.2736 18.3052 18.8576 18.4853 16.0257L18.7448 11.9452C18.8425 10.4086 18.8913 9.64037 18.4498 9.15352C18.0082 8.66667 17.2625 8.66667 15.7712 8.66667H8.22884C6.7375 8.66667 5.99183 8.66667 5.55026 9.15352C5.1087 9.64037 5.15756 10.4086 5.25528 11.9452L5.51479 16.0257C5.69489 18.8576 5.78494 20.2736 6.66513 21.1368C7.54532 22 8.89906 22 11.6066 22Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func DocumentSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var13 == nil {
|
||||
templ_7745c5c3_Var13 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var14 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M6.27103 2.11151C5.46135 2.21816 5.03258 2.41324 4.72718 2.71244C4.42179 3.01165 4.22268 3.43172 4.11382 4.225C4.00176 5.04159 4 6.12387 4 7.67568V16.2442C4.38867 15.9781 4.82674 15.7756 5.29899 15.6517C5.82716 15.513 6.44305 15.5132 7.34563 15.5135L20 15.5135V7.67568C20 6.12387 19.9982 5.04159 19.8862 4.22499C19.7773 3.43172 19.5782 3.01165 19.2728 2.71244C18.9674 2.41324 18.5387 2.21816 17.729 2.11151C16.8955 2.00172 15.7908 2 14.2069 2H9.7931C8.2092 2 7.10452 2.00172 6.27103 2.11151ZM6.75862 6.59459C6.75862 6.1468 7.12914 5.78378 7.58621 5.78378H16.4138C16.8709 5.78378 17.2414 6.1468 17.2414 6.59459C17.2414 7.04239 16.8709 7.40541 16.4138 7.40541H7.58621C7.12914 7.40541 6.75862 7.04239 6.75862 6.59459ZM7.58621 9.56757C7.12914 9.56757 6.75862 9.93058 6.75862 10.3784C6.75862 10.8262 7.12914 11.1892 7.58621 11.1892H13.1034C13.5605 11.1892 13.931 10.8262 13.931 10.3784C13.931 9.93058 13.5605 9.56757 13.1034 9.56757H7.58621Z\"></path> <path d=\"M7.47341 17.1351H8.68965H13.1034H19.9991C19.9956 18.2657 19.9776 19.1088 19.8862 19.775C19.7773 20.5683 19.5782 20.9884 19.2728 21.2876C18.9674 21.5868 18.5387 21.7818 17.729 21.8885C16.8955 21.9983 15.7908 22 14.2069 22H9.7931C8.2092 22 7.10452 21.9983 6.27103 21.8885C5.46135 21.7818 5.03258 21.5868 4.72718 21.2876C4.42179 20.9884 4.22268 20.5683 4.11382 19.775C4.07259 19.4746 4.0463 19.1382 4.02952 18.7558C4.30088 18.0044 4.93365 17.4264 5.72738 17.218C6.01657 17.1421 6.39395 17.1351 7.47341 17.1351Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func DownloadSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var16 == nil {
|
||||
templ_7745c5c3_Var16 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var17 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var17).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25ZM8 16.25C7.58579 16.25 7.25 16.5858 7.25 17C7.25 17.4142 7.58579 17.75 8 17.75H16C16.4142 17.75 16.75 17.4142 16.75 17C16.75 16.5858 16.4142 16.25 16 16.25H8Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func DropdownSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var19 == nil {
|
||||
templ_7745c5c3_Var19 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var20 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var20).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" viewBox=\"0 0 1792 1792\" fill=\"currentColor\"><path d=\"M1408 704q0 26-19 45l-448 448q-19 19-45 19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func EditSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var22 == nil {
|
||||
templ_7745c5c3_Var22 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var23 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var23).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z\"></path> <path d=\"M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z\"></path> <path d=\"M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GitSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var25 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var25 == nil {
|
||||
templ_7745c5c3_Var25 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var26 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var26...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var26).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" fill=\"currentColor\" viewBox=\"0 0 219 92\"><defs><clipPath id=\"a\"><path d=\"M159 .79h25V69h-25Zm0 0\"></path></clipPath> <clipPath id=\"b\"><path d=\"M183 9h35.371v60H183Zm0 0\"></path></clipPath> <clipPath id=\"c\"><path d=\"M0 .79h92V92H0Zm0 0\"></path></clipPath></defs> <path style=\"stroke: none; fill-rule: nonzero; fill-opacity: 1\" d=\"M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61\"></path> <g clip-path=\"url(#a)\"><path style=\"stroke: none; fill-rule: nonzero; fill-opacity: 1\" d=\"M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805\"></path></g> <g clip-path=\"url(#b)\"><path style=\"stroke: none; fill-rule: nonzero; fill-opacity: 1\" d=\"M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66\"></path></g> <g clip-path=\"url(#c)\"><path style=\"stroke: none; fill-rule: nonzero; fill-opacity: 1\" d=\"M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305\"></path></g></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func HomeSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var28 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var28 == nil {
|
||||
templ_7745c5c3_Var28 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var29 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var29).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2.5192 7.82274C2 8.77128 2 9.91549 2 12.2039V13.725C2 17.6258 2 19.5763 3.17157 20.7881C4.34315 22 6.22876 22 10 22H14C17.7712 22 19.6569 22 20.8284 20.7881C22 19.5763 22 17.6258 22 13.725V12.2039C22 9.91549 22 8.77128 21.4808 7.82274C20.9616 6.87421 20.0131 6.28551 18.116 5.10812L16.116 3.86687C14.1106 2.62229 13.1079 2 12 2C10.8921 2 9.88939 2.62229 7.88403 3.86687L5.88403 5.10813C3.98695 6.28551 3.0384 6.87421 2.5192 7.82274ZM11.25 18C11.25 18.4142 11.5858 18.75 12 18.75C12.4142 18.75 12.75 18.4142 12.75 18V15C12.75 14.5858 12.4142 14.25 12 14.25C11.5858 14.25 11.25 14.5858 11.25 15V18Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ImportSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var31 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var31 == nil {
|
||||
templ_7745c5c3_Var31 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var32 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var32...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var32).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2.06935 5.00839C2 5.37595 2 5.81722 2 6.69975V13.75C2 17.5212 2 19.4069 3.17157 20.5784C4.34315 21.75 6.22876 21.75 10 21.75H14C17.7712 21.75 19.6569 21.75 20.8284 20.5784C22 19.4069 22 17.5212 22 13.75V11.5479C22 8.91554 22 7.59935 21.2305 6.74383C21.1598 6.66514 21.0849 6.59024 21.0062 6.51946C20.1506 5.75 18.8345 5.75 16.2021 5.75H15.8284C14.6747 5.75 14.0979 5.75 13.5604 5.59678C13.2651 5.5126 12.9804 5.39471 12.7121 5.24543C12.2237 4.97367 11.8158 4.56578 11 3.75L10.4497 3.19975C10.1763 2.92633 10.0396 2.78961 9.89594 2.67051C9.27652 2.15704 8.51665 1.84229 7.71557 1.76738C7.52976 1.75 7.33642 1.75 6.94975 1.75C6.06722 1.75 5.62595 1.75 5.25839 1.81935C3.64031 2.12464 2.37464 3.39031 2.06935 5.00839ZM12 11C12.4142 11 12.75 11.3358 12.75 11.75V13H14C14.4142 13 14.75 13.3358 14.75 13.75C14.75 14.1642 14.4142 14.5 14 14.5H12.75V15.75C12.75 16.1642 12.4142 16.5 12 16.5C11.5858 16.5 11.25 16.1642 11.25 15.75V14.5H10C9.58579 14.5 9.25 14.1642 9.25 13.75C9.25 13.3358 9.58579 13 10 13H11.25V11.75C11.25 11.3358 11.5858 11 12 11Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func InfoSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var34 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var34 == nil {
|
||||
templ_7745c5c3_Var34 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var35 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var35...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var35).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 17.75C12.4142 17.75 12.75 17.4142 12.75 17V11C12.75 10.5858 12.4142 10.25 12 10.25C11.5858 10.25 11.25 10.5858 11.25 11V17C11.25 17.4142 11.5858 17.75 12 17.75ZM12 7C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func LoadingSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var37 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var37 == nil {
|
||||
templ_7745c5c3_Var37 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var38 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var38...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var38).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><style>\n\t\t .spinner_l9ve {\n\t\t animation: spinner_rcyq 1.2s cubic-bezier(0.52, 0.6, 0.25, 0.99) infinite;\n\t\t }\n\t\t .spinner_cMYp {\n\t\t animation-delay: 0.4s;\n\t\t }\n\t\t .spinner_gHR3 {\n\t\t animation-delay: 0.8s;\n\t\t }\n\t\t @keyframes spinner_rcyq {\n\t\t 0% {\n\t\t transform: translate(12px, 12px) scale(0);\n\t\t opacity: 1;\n\t\t }\n\t\t 100% {\n\t\t transform: translate(0, 0) scale(1);\n\t\t opacity: 0;\n\t\t }\n\t\t }\n\t\t</style><path class=\"spinner_l9ve\" d=\"M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z\" transform=\"translate(12, 12) scale(0)\"></path> <path class=\"spinner_l9ve spinner_cMYp\" d=\"M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z\" transform=\"translate(12, 12) scale(0)\"></path> <path class=\"spinner_l9ve spinner_gHR3\" d=\"M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z\" transform=\"translate(12, 12) scale(0)\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func PasswordSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var40 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var40 == nil {
|
||||
templ_7745c5c3_Var40 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var41 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var41...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var42 string
|
||||
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var41).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" fill=\"currentColor\" viewBox=\"0 0 1792 1792\"><path d=\"M1376 768q40 0 68 28t28 68v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h32v-320q0-185 131.5-316.5t316.5-131.5 316.5 131.5 131.5 316.5q0 26-19 45t-45 19h-64q-26 0-45-19t-19-45q0-106-75-181t-181-75-181 75-75 181v320h736z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func SearchSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var43 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var43 == nil {
|
||||
templ_7745c5c3_Var43 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var44 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var44...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var45 string
|
||||
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var44).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM9 11.5C9 10.1193 10.1193 9 11.5 9C12.8807 9 14 10.1193 14 11.5C14 12.8807 12.8807 14 11.5 14C10.1193 14 9 12.8807 9 11.5ZM11.5 7C9.01472 7 7 9.01472 7 11.5C7 13.9853 9.01472 16 11.5 16C12.3805 16 13.202 15.7471 13.8957 15.31L15.2929 16.7071C15.6834 17.0976 16.3166 17.0976 16.7071 16.7071C17.0976 16.3166 17.0976 15.6834 16.7071 15.2929L15.31 13.8957C15.7471 13.202 16 12.3805 16 11.5C16 9.01472 13.9853 7 11.5 7Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Search2SVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var46 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var46 == nil {
|
||||
templ_7745c5c3_Var46 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var47 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var47...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var48 string
|
||||
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var47).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><rect width=\"24\" height=\"24\" fill=\"none\"></rect> <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C11.8487 18 13.551 17.3729 14.9056 16.3199L20.2929 21.7071C20.6834 22.0976 21.3166 22.0976 21.7071 21.7071C22.0976 21.3166 22.0976 20.6834 21.7071 20.2929L16.3199 14.9056C17.3729 13.551 18 11.8487 18 10C18 5.58172 14.4183 2 10 2Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func SettingsSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var49 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var49 == nil {
|
||||
templ_7745c5c3_Var49 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var50 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var50...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var51 string
|
||||
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var50).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M14.2788 2.15224C13.9085 2 13.439 2 12.5 2C11.561 2 11.0915 2 10.7212 2.15224C10.2274 2.35523 9.83509 2.74458 9.63056 3.23463C9.53719 3.45834 9.50065 3.7185 9.48635 4.09799C9.46534 4.65568 9.17716 5.17189 8.69017 5.45093C8.20318 5.72996 7.60864 5.71954 7.11149 5.45876C6.77318 5.2813 6.52789 5.18262 6.28599 5.15102C5.75609 5.08178 5.22018 5.22429 4.79616 5.5472C4.47814 5.78938 4.24339 6.1929 3.7739 6.99993C3.30441 7.80697 3.06967 8.21048 3.01735 8.60491C2.94758 9.1308 3.09118 9.66266 3.41655 10.0835C3.56506 10.2756 3.77377 10.437 4.0977 10.639C4.57391 10.936 4.88032 11.4419 4.88029 12C4.88026 12.5581 4.57386 13.0639 4.0977 13.3608C3.77372 13.5629 3.56497 13.7244 3.41645 13.9165C3.09108 14.3373 2.94749 14.8691 3.01725 15.395C3.06957 15.7894 3.30432 16.193 3.7738 17C4.24329 17.807 4.47804 18.2106 4.79606 18.4527C5.22008 18.7756 5.75599 18.9181 6.28589 18.8489C6.52778 18.8173 6.77305 18.7186 7.11133 18.5412C7.60852 18.2804 8.2031 18.27 8.69012 18.549C9.17714 18.8281 9.46533 19.3443 9.48635 19.9021C9.50065 20.2815 9.53719 20.5417 9.63056 20.7654C9.83509 21.2554 10.2274 21.6448 10.7212 21.8478C11.0915 22 11.561 22 12.5 22C13.439 22 13.9085 22 14.2788 21.8478C14.7726 21.6448 15.1649 21.2554 15.3694 20.7654C15.4628 20.5417 15.4994 20.2815 15.5137 19.902C15.5347 19.3443 15.8228 18.8281 16.3098 18.549C16.7968 18.2699 17.3914 18.2804 17.8886 18.5412C18.2269 18.7186 18.4721 18.8172 18.714 18.8488C19.2439 18.9181 19.7798 18.7756 20.2038 18.4527C20.5219 18.2105 20.7566 17.807 21.2261 16.9999C21.6956 16.1929 21.9303 15.7894 21.9827 15.395C22.0524 14.8691 21.9088 14.3372 21.5835 13.9164C21.4349 13.7243 21.2262 13.5628 20.9022 13.3608C20.4261 13.0639 20.1197 12.558 20.1197 11.9999C20.1197 11.4418 20.4261 10.9361 20.9022 10.6392C21.2263 10.4371 21.435 10.2757 21.5836 10.0835C21.9089 9.66273 22.0525 9.13087 21.9828 8.60497C21.9304 8.21055 21.6957 7.80703 21.2262 7C20.7567 6.19297 20.522 5.78945 20.2039 5.54727C19.7799 5.22436 19.244 5.08185 18.7141 5.15109C18.4722 5.18269 18.2269 5.28136 17.8887 5.4588C17.3915 5.71959 16.7969 5.73002 16.3099 5.45096C15.8229 5.17191 15.5347 4.65566 15.5136 4.09794C15.4993 3.71848 15.4628 3.45833 15.3694 3.23463C15.1649 2.74458 14.7726 2.35523 14.2788 2.15224ZM12.5 15C14.1695 15 15.5228 13.6569 15.5228 12C15.5228 10.3431 14.1695 9 12.5 9C10.8305 9 9.47716 10.3431 9.47716 12C9.47716 13.6569 10.8305 15 12.5 15Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func UploadSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var52 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var52 == nil {
|
||||
templ_7745c5c3_Var52 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var53 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var53...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var54 string
|
||||
templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var53).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 15.75C12.4142 15.75 12.75 15.4142 12.75 15V4.02744L14.4306 5.98809C14.7001 6.30259 15.1736 6.33901 15.4881 6.06944C15.8026 5.79988 15.839 5.3264 15.5694 5.01191L12.5694 1.51191C12.427 1.34567 12.2189 1.25 12 1.25C11.7811 1.25 11.573 1.34567 11.4306 1.51191L8.43056 5.01191C8.16099 5.3264 8.19741 5.79988 8.51191 6.06944C8.8264 6.33901 9.29988 6.30259 9.56944 5.98809L11.25 4.02744L11.25 15C11.25 15.4142 11.5858 15.75 12 15.75Z\"></path> <path d=\"M16 9C15.2978 9 14.9467 9 14.6945 9.16851C14.5853 9.24148 14.4915 9.33525 14.4186 9.44446C14.25 9.69667 14.25 10.0478 14.25 10.75L14.25 15C14.25 16.2426 13.2427 17.25 12 17.25C10.7574 17.25 9.75004 16.2426 9.75004 15L9.75004 10.75C9.75004 10.0478 9.75004 9.69664 9.58149 9.4444C9.50854 9.33523 9.41481 9.2415 9.30564 9.16855C9.05341 9 8.70227 9 8 9C5.17157 9 3.75736 9 2.87868 9.87868C2 10.7574 2 12.1714 2 14.9998V15.9998C2 18.8282 2 20.2424 2.87868 21.1211C3.75736 21.9998 5.17157 21.9998 8 21.9998H16C18.8284 21.9998 20.2426 21.9998 21.1213 21.1211C22 20.2424 22 18.8282 22 15.9998V14.9998C22 12.1714 22 10.7574 21.1213 9.87868C20.2426 9 18.8284 9 16 9Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func UserSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var55 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var55 == nil {
|
||||
templ_7745c5c3_Var55 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var56 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var56...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var57 string
|
||||
templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var56).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" fill=\"currentColor\" viewBox=\"0 0 1792 1792\"><path d=\"M1523 1339q-22-155-87.5-257.5t-184.5-118.5q-67 74-159.5 115.5t-195.5 41.5-195.5-41.5-159.5-115.5q-119 16-184.5 118.5t-87.5 257.5q106 150 271 237.5t356 87.5 356-87.5 271-237.5zm-243-699q0-159-112.5-271.5t-271.5-112.5-271.5 112.5-112.5 271.5 112.5 271.5 271.5 112.5 271.5-112.5 112.5-271.5zm512 256q0 182-71 347.5t-190.5 286-285.5 191.5-349 71q-182 0-348-71t-286-191-191-286-71-348 71-348 191-286 286-191 348-71 348 71 286 191 191 286 71 348z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
49
ngtemplates/components/table.templ
Normal file
49
ngtemplates/components/table.templ
Normal file
@@ -0,0 +1,49 @@
|
||||
package components
|
||||
|
||||
type TableCellFormatter[T any] func(data T) templ.Component
|
||||
|
||||
type TableColumn[T any] struct {
|
||||
Name string // Column Name
|
||||
Getter func(T) string // Data Getter
|
||||
Formatter TableCellFormatter[T] // Data Formatter
|
||||
}
|
||||
|
||||
templ (c *TableColumn[T]) getCell(d T) {
|
||||
if c.Formatter != nil {
|
||||
@c.Formatter(d)
|
||||
} else if c.Getter != nil {
|
||||
{ c.Getter(d) }
|
||||
} else {
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
templ Table[T any](columns []TableColumn[T], rows []T) {
|
||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||
<thead class="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
for _, column := range columns {
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||
{ column.Name }
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-black dark:text-white">
|
||||
if len(rows) == 0 {
|
||||
<tr>
|
||||
<td class="text-center p-3" colspan="4">No Results</td>
|
||||
</tr>
|
||||
}
|
||||
for _, row := range rows {
|
||||
<tr>
|
||||
for _, column := range columns {
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
@column.getCell(row)
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
151
ngtemplates/components/table_templ.go
Normal file
151
ngtemplates/components/table_templ.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
type TableCellFormatter[T any] func(data T) templ.Component
|
||||
|
||||
type TableColumn[T any] struct {
|
||||
Name string // Column Name
|
||||
Getter func(T) string // Data Getter
|
||||
Formatter TableCellFormatter[T] // Data Formatter
|
||||
}
|
||||
|
||||
func (c *TableColumn[T]) getCell(d T) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if c.Formatter != nil {
|
||||
templ_7745c5c3_Err = c.Formatter(d).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if c.Getter != nil {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(c.Getter(d))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/table.templ`, Line: 15, Col: 15}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "\"Unknown\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Table[T any](columns []TableColumn[T], rows []T) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<table class=\"min-w-full leading-normal bg-white dark:bg-gray-700 text-sm\"><thead class=\"text-gray-800 dark:text-gray-400\"><tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, column := range columns {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<th class=\"p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(column.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/table.templ`, Line: 27, Col: 19}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</th>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</tr></thead> <tbody class=\"text-black dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<tr><td class=\"text-center p-3\" colspan=\"4\">No Results</td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, row := range rows {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, column := range columns {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<td class=\"p-3 border-b border-gray-200\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = column.getCell(row).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</tbody></table>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
51
ngtemplates/pages/activity.templ
Normal file
51
ngtemplates/pages/activity.templ
Normal file
@@ -0,0 +1,51 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
"reichard.io/antholume/ngtemplates/components"
|
||||
)
|
||||
|
||||
var columns = []components.TableColumn[database.GetActivityRow]{
|
||||
{
|
||||
Name: "Document",
|
||||
Formatter: documentFormatter,
|
||||
},
|
||||
{
|
||||
Name: "Time",
|
||||
Getter: func(r database.GetActivityRow) string {
|
||||
return fmt.Sprint(r.StartTime)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Duration",
|
||||
Getter: func(r database.GetActivityRow) string {
|
||||
return fmt.Sprint(r.Duration)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Percent",
|
||||
Formatter: percentageFormatter,
|
||||
},
|
||||
}
|
||||
|
||||
templ documentFormatter(row database.GetActivityRow) {
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("./documents/%s", row.DocumentID)) }>
|
||||
{ fmt.Sprintf("%s - %s", *row.Author, *row.Title) }
|
||||
</a>
|
||||
}
|
||||
|
||||
templ percentageFormatter(row database.GetActivityRow) {
|
||||
{ fmt.Sprintf("%.2f%%", row.EndPercentage) }
|
||||
}
|
||||
|
||||
templ Activity(settings common.Settings, rows []database.GetActivityRow) {
|
||||
@layout(settings, "Activity") {
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
@components.Table(columns, rows)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
181
ngtemplates/pages/activity_templ.go
Normal file
181
ngtemplates/pages/activity_templ.go
Normal file
@@ -0,0 +1,181 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package pages
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
"reichard.io/antholume/ngtemplates/components"
|
||||
)
|
||||
|
||||
var columns = []components.TableColumn[database.GetActivityRow]{
|
||||
{
|
||||
Name: "Document",
|
||||
Formatter: documentFormatter,
|
||||
},
|
||||
{
|
||||
Name: "Time",
|
||||
Getter: func(r database.GetActivityRow) string {
|
||||
return fmt.Sprint(r.StartTime)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Duration",
|
||||
Getter: func(r database.GetActivityRow) string {
|
||||
return fmt.Sprint(r.Duration)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Percent",
|
||||
Formatter: percentageFormatter,
|
||||
},
|
||||
}
|
||||
|
||||
func documentFormatter(row database.GetActivityRow) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL = templ.SafeURL(fmt.Sprintf("./documents/%s", row.DocumentID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s - %s", *row.Author, *row.Title))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/activity.templ`, Line: 35, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func percentageFormatter(row database.GetActivityRow) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f%%", row.EndPercentage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/activity.templ`, Line: 40, Col: 43}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Activity(settings common.Settings, rows []database.GetActivityRow) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var6 == nil {
|
||||
templ_7745c5c3_Var6 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var7 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"overflow-x-auto\"><div class=\"inline-block min-w-full overflow-hidden rounded shadow\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Table(columns, rows).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout(settings, "Activity").Render(templ.WithChildren(ctx, templ_7745c5c3_Var7), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
102
ngtemplates/pages/documents.templ
Normal file
102
ngtemplates/pages/documents.templ
Normal file
@@ -0,0 +1,102 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
"reichard.io/antholume/ngtemplates/components"
|
||||
)
|
||||
|
||||
templ Documents(settings common.Settings, documents []database.GetDocumentsWithStatsRow, nextPage, previousPage, totalPages, pageLimit int64) {
|
||||
@layout(settings, "Activity") {
|
||||
<div
|
||||
class="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<form
|
||||
class="flex gap-4 flex-col lg:flex-row mb-0"
|
||||
action="./documents"
|
||||
method="GET"
|
||||
>
|
||||
<div class="flex flex-col w-full grow">
|
||||
<div class="flex relative">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
@components.Search2SVG("size-4")
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
name="search"
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
placeholder="Search Author / Title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:w-60">
|
||||
@components.Button("Search", components.ButtonVariantSecondary)
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
for _, doc := range documents {
|
||||
@components.DocumentCard(doc)
|
||||
}
|
||||
</div>
|
||||
<div class="w-full flex gap-4 justify-center mt-4 text-black dark:text-white">
|
||||
if previousPage > 0 {
|
||||
<a
|
||||
href={ templ.SafeURL(fmt.Sprintf("./documents?page=%d&limit=%d", previousPage, pageLimit)) }
|
||||
class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none"
|
||||
>◄</a>
|
||||
}
|
||||
if nextPage <= totalPages {
|
||||
<a
|
||||
href={ templ.SafeURL(fmt.Sprintf("./documents?page=%d&limit=%d", nextPage, pageLimit)) }
|
||||
class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none"
|
||||
>►</a>
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
class="fixed bottom-6 right-6 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<input type="checkbox" id="upload-file-button" class="hidden css-button"/>
|
||||
<div
|
||||
class="absolute right-0 z-10 bottom-0 rounded p-4 bg-gray-800 dark:bg-gray-200 text-white dark:text-black w-72 text-sm flex flex-col gap-2"
|
||||
>
|
||||
<form
|
||||
method="POST"
|
||||
enctype="multipart/form-data"
|
||||
action="./documents"
|
||||
class="flex flex-col gap-2 mb-0"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept=".epub"
|
||||
id="document_file"
|
||||
name="document_file"
|
||||
/>
|
||||
<button
|
||||
class="font-medium px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
type="submit"
|
||||
>
|
||||
Upload File
|
||||
</button>
|
||||
</form>
|
||||
<label for="upload-file-button">
|
||||
<div
|
||||
class="w-full text-center cursor-pointer font-medium mt-2 px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
Cancel Upload
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<label
|
||||
class="w-16 h-16 bg-gray-800 dark:bg-gray-200 rounded-full flex items-center justify-center opacity-30 hover:opacity-100 transition-all duration-200 cursor-pointer"
|
||||
for="upload-file-button"
|
||||
>
|
||||
@components.UploadSVG("size-8")
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
133
ngtemplates/pages/documents_templ.go
Normal file
133
ngtemplates/pages/documents_templ.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package pages
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
"reichard.io/antholume/ngtemplates/components"
|
||||
)
|
||||
|
||||
func Documents(settings common.Settings, documents []database.GetDocumentsWithStatsRow, nextPage, previousPage, totalPages, pageLimit int64) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white\"><form class=\"flex gap-4 flex-col lg:flex-row mb-0\" action=\"./documents\" method=\"GET\"><div class=\"flex flex-col w-full grow\"><div class=\"flex relative\"><span class=\"inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Search2SVG("size-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span> <input type=\"text\" id=\"search\" name=\"search\" class=\"flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent\" placeholder=\"Search Author / Title\"></div></div><div class=\"lg:w-60\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Button("Search", components.ButtonVariantSecondary).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></form></div><div class=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, doc := range documents {
|
||||
templ_7745c5c3_Err = components.DocumentCard(doc).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div class=\"w-full flex gap-4 justify-center mt-4 text-black dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if previousPage > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 templ.SafeURL = templ.SafeURL(fmt.Sprintf("./documents?page=%d&limit=%d", previousPage, pageLimit))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none\">◄</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if nextPage <= totalPages {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 templ.SafeURL = templ.SafeURL(fmt.Sprintf("./documents?page=%d&limit=%d", nextPage, pageLimit))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none\">►</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><div class=\"fixed bottom-6 right-6 rounded-full flex items-center justify-center\"><input type=\"checkbox\" id=\"upload-file-button\" class=\"hidden css-button\"><div class=\"absolute right-0 z-10 bottom-0 rounded p-4 bg-gray-800 dark:bg-gray-200 text-white dark:text-black w-72 text-sm flex flex-col gap-2\"><form method=\"POST\" enctype=\"multipart/form-data\" action=\"./documents\" class=\"flex flex-col gap-2 mb-0\"><input type=\"file\" accept=\".epub\" id=\"document_file\" name=\"document_file\"> <button class=\"font-medium px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800\" type=\"submit\">Upload File</button></form><label for=\"upload-file-button\"><div class=\"w-full text-center cursor-pointer font-medium mt-2 px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800\">Cancel Upload</div></label></div><label class=\"w-16 h-16 bg-gray-800 dark:bg-gray-200 rounded-full flex items-center justify-center opacity-30 hover:opacity-100 transition-all duration-200 cursor-pointer\" for=\"upload-file-button\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.UploadSVG("size-8").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</label></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout(settings, "Activity").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
40
ngtemplates/pages/home.templ
Normal file
40
ngtemplates/pages/home.templ
Normal file
@@ -0,0 +1,40 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/graph"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
"reichard.io/antholume/ngtemplates/components"
|
||||
)
|
||||
|
||||
templ Home(
|
||||
settings common.Settings,
|
||||
dailyReadSVG graph.SVGGraphData,
|
||||
userStreaks []database.UserStreak,
|
||||
userStatistics common.UserStatistics,
|
||||
userMetadata common.UserMetadata,
|
||||
) {
|
||||
@layout(settings, "Home") {
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="w-full">
|
||||
@components.DailyReadChart(dailyReadSVG)
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
@components.InfoCardLink("Documents", userMetadata.DocumentCount, "./documents")
|
||||
@components.InfoCardLink("Activity Records", userMetadata.ActivityCount, "./activity")
|
||||
@components.InfoCardLink("Progress Records", userMetadata.ProgressCount, "./progress")
|
||||
@components.InfoCard("Devices", userMetadata.DeviceCount)
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
for _, item := range userStreaks {
|
||||
@components.StreakCard(item)
|
||||
}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
@components.LeaderboardCard("WPM", userStatistics.WPM)
|
||||
@components.LeaderboardCard("Duration", userStatistics.Duration)
|
||||
@components.LeaderboardCard("Words", userStatistics.Words)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
125
ngtemplates/pages/home_templ.go
Normal file
125
ngtemplates/pages/home_templ.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package pages
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/graph"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
"reichard.io/antholume/ngtemplates/components"
|
||||
)
|
||||
|
||||
func Home(
|
||||
settings common.Settings,
|
||||
dailyReadSVG graph.SVGGraphData,
|
||||
userStreaks []database.UserStreak,
|
||||
userStatistics common.UserStatistics,
|
||||
userMetadata common.UserMetadata,
|
||||
) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex flex-col gap-4\"><div class=\"w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.DailyReadChart(dailyReadSVG).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div class=\"grid grid-cols-2 gap-4 md:grid-cols-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.InfoCardLink("Documents", userMetadata.DocumentCount, "./documents").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.InfoCardLink("Activity Records", userMetadata.ActivityCount, "./activity").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.InfoCardLink("Progress Records", userMetadata.ProgressCount, "./progress").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.InfoCard("Devices", userMetadata.DeviceCount).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><div class=\"grid grid-cols-1 gap-4 md:grid-cols-2\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range userStreaks {
|
||||
templ_7745c5c3_Err = components.StreakCard(item).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div class=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.LeaderboardCard("WPM", userStatistics.WPM).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.LeaderboardCard("Duration", userStatistics.Duration).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.LeaderboardCard("Words", userStatistics.Words).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout(settings, "Home").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
375
ngtemplates/pages/layout.templ
Normal file
375
ngtemplates/pages/layout.templ
Normal file
@@ -0,0 +1,375 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/ngtemplates/components"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
)
|
||||
|
||||
func getNavigationLinkClass(isActive bool) string {
|
||||
defaultClass := "flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4"
|
||||
if isActive {
|
||||
return fmt.Sprintf("%s border-purple-500 dark:text-white", defaultClass)
|
||||
} else {
|
||||
return fmt.Sprintf("%s border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", defaultClass)
|
||||
}
|
||||
}
|
||||
|
||||
templ navigation(settings common.Settings) {
|
||||
<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"
|
||||
/>
|
||||
<span
|
||||
class="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white"
|
||||
></span>
|
||||
<span
|
||||
class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"
|
||||
></span>
|
||||
<span
|
||||
class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"
|
||||
></span>
|
||||
<div
|
||||
id="menu"
|
||||
class="fixed -ml-6 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-8 lg:pr-0">
|
||||
AnthoLume
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/" class={ getNavigationLinkClass(settings.Route == common.RouteHome) }>
|
||||
@components.HomeSVG("size-5")
|
||||
<span class="mx-4 text-sm font-normal">Home</span>
|
||||
</a>
|
||||
<a href="/documents" class={ getNavigationLinkClass(settings.Route == common.RouteDocuments) }>
|
||||
@components.DocumentSVG("size-5")
|
||||
<span class="mx-4 text-sm font-normal">Documents</span>
|
||||
</a>
|
||||
<a href="/progress" class={ getNavigationLinkClass(settings.Route == common.RouteProgress) }>
|
||||
@components.ActivitySVG("size-5")
|
||||
<span class="mx-4 text-sm font-normal">Progress</span>
|
||||
</a>
|
||||
<a href="/activity" class={ getNavigationLinkClass(settings.Route == common.RouteActivity) }>
|
||||
@components.ActivitySVG("size-5")
|
||||
<span class="mx-4 text-sm font-normal">Activity</span>
|
||||
</a>
|
||||
if settings.SearchEnabled {
|
||||
<a href="/search" class={ getNavigationLinkClass(settings.Route == common.RouteSearch) }>
|
||||
@components.SearchSVG("size-5")
|
||||
<span class="mx-4 text-sm font-normal">Search</span>
|
||||
</a>
|
||||
}
|
||||
if settings.IsAdmin {
|
||||
<div
|
||||
class={
|
||||
"flex flex-col gap-4 p-2 pl-6 my-2 transition-colors duration-200 border-l-4",
|
||||
templ.KV("dark:text-white border-purple-500", settings.Route.IsAdmin()),
|
||||
templ.KV("border-transparent text-gray-400", !settings.Route.IsAdmin()),
|
||||
}
|
||||
>
|
||||
<a
|
||||
href="/admin"
|
||||
class={
|
||||
"flex justify-start w-full",
|
||||
templ.KV("text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", !settings.Route.IsAdmin()),
|
||||
}
|
||||
>
|
||||
@components.SettingsSVG("size-5")
|
||||
<span class="mx-4 text-sm font-normal">Admin</span>
|
||||
</a>
|
||||
if settings.Route.IsAdmin() {
|
||||
<a
|
||||
href="/admin"
|
||||
style="padding-left: 1.75em"
|
||||
class={
|
||||
"flex justify-start w-full",
|
||||
templ.KV("text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", settings.Route != common.RouteAdmin),
|
||||
}
|
||||
>
|
||||
<span class="mx-4 text-sm font-normal">General</span>
|
||||
</a>
|
||||
<a
|
||||
href="/admin/import"
|
||||
style="padding-left: 1.75em"
|
||||
class={
|
||||
"flex justify-start w-full",
|
||||
templ.KV("text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", settings.Route != common.RouteAdminImport),
|
||||
}
|
||||
>
|
||||
<span class="mx-4 text-sm font-normal">Import</span>
|
||||
</a>
|
||||
<a
|
||||
href="/admin/users"
|
||||
style="padding-left: 1.75em"
|
||||
class={
|
||||
"flex justify-start w-full",
|
||||
templ.KV("text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", settings.Route != common.RouteAdminUsers),
|
||||
}
|
||||
>
|
||||
<span class="mx-4 text-sm font-normal">Users</span>
|
||||
</a>
|
||||
<a
|
||||
href="/admin/logs"
|
||||
style="padding-left: 1.75em"
|
||||
class={
|
||||
"flex justify-start w-full",
|
||||
templ.KV("text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", settings.Route != common.RouteAdminLogs),
|
||||
}
|
||||
>
|
||||
<span class="mx-4 text-sm font-normal">Logs</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<a
|
||||
class="flex flex-col gap-2 justify-center items-center p-6 w-full absolute bottom-0 text-black dark:text-white"
|
||||
target="_blank"
|
||||
href="https://gitea.va.reichard.io/evan/AnthoLume"
|
||||
>
|
||||
@components.GitSVG("h-5 text-black dark:text-white")
|
||||
<span class="text-xs">{ settings.Version }</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold dark:text-white px-6 lg:ml-44">
|
||||
{ settings.Route.Name() }
|
||||
</h1>
|
||||
<div class="relative flex items-center justify-end w-full p-4 space-x-4">
|
||||
<a href="#" class="relative block text-gray-800 dark:text-gray-200">
|
||||
@components.UserSVG("size-5")
|
||||
</a>
|
||||
<input type="checkbox" id="user-dropdown-button" class="hidden"/>
|
||||
<div
|
||||
id="user-dropdown"
|
||||
class="transition duration-200 z-20 absolute right-4 top-16 pt-4"
|
||||
>
|
||||
<div
|
||||
class="w-40 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div
|
||||
class="py-1"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="options-menu"
|
||||
>
|
||||
<a
|
||||
href="/settings"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||
role="menuitem"
|
||||
>
|
||||
<span class="flex flex-col">
|
||||
<span>Settings</span>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/local"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||
role="menuitem"
|
||||
>
|
||||
<span class="flex flex-col">
|
||||
<span>Offline</span>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/logout"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||
role="menuitem"
|
||||
>
|
||||
<span class="flex flex-col">
|
||||
<span>Logout</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label for="user-dropdown-button">
|
||||
<div
|
||||
class="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer"
|
||||
>
|
||||
<span>{ settings.User }</span>
|
||||
<span class="text-gray-800 dark:text-gray-200">
|
||||
@components.DropdownSVG("size-5")
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ layout(settings common.Settings, title string) {
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#F3F4F6"
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#1F2937"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<title>AnthoLume - { title }</title>
|
||||
<link rel="manifest" href="/manifest.json"/>
|
||||
<link rel="stylesheet" href="/assets/style.css"/>
|
||||
<!-- Service Worker / Offline Cache Flush -->
|
||||
<script src="/assets/lib/idb-keyval.min.js"></script>
|
||||
<script src="/assets/common.js"></script>
|
||||
<script src="/assets/index.js"></script>
|
||||
<style>
|
||||
/* ----------------------------- */
|
||||
/* -------- PWA Styling -------- */
|
||||
/* ----------------------------- */
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior-y: none;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
html {
|
||||
height: calc(100% + env(safe-area-inset-bottom));
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0
|
||||
env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
main {
|
||||
height: calc(100dvh - 4rem - env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
#container {
|
||||
padding-bottom: calc(5em + env(safe-area-inset-bottom) * 2);
|
||||
}
|
||||
|
||||
/* No Scrollbar - IE, Edge, Firefox */
|
||||
* {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* No Scrollbar - WebKit */
|
||||
*::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ----------------------------- */
|
||||
/* -------- CSS Button -------- */
|
||||
/* ----------------------------- */
|
||||
.css-button:checked + div {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.css-button + div {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ----------------------------- */
|
||||
/* ------- User Dropdown ------- */
|
||||
/* ----------------------------- */
|
||||
#user-dropdown-button:checked + #user-dropdown {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#user-dropdown {
|
||||
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),
|
||||
background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1),
|
||||
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);
|
||||
}
|
||||
|
||||
#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 {
|
||||
top: 0;
|
||||
padding-top: env(safe-area-inset-top);
|
||||
transform-origin: 0% 0%;
|
||||
transform: translate(-100%, 0);
|
||||
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1);
|
||||
}
|
||||
|
||||
@media (orientation: landscape) {
|
||||
#menu {
|
||||
transform: translate(
|
||||
calc(-1 * (env(safe-area-inset-left) + 100%)),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-gray-800">
|
||||
@navigation(settings)
|
||||
<main class="relative overflow-hidden">
|
||||
<div
|
||||
id="container"
|
||||
class="h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48"
|
||||
>
|
||||
{ children... }
|
||||
</div>
|
||||
</main>
|
||||
<div class="absolute right-4 bottom-4">
|
||||
<!--
|
||||
<div class="w-72 p-4 bg-red-500 rounded-xl">
|
||||
<span>User Deleted</span>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
499
ngtemplates/pages/layout_templ.go
Normal file
499
ngtemplates/pages/layout_templ.go
Normal file
@@ -0,0 +1,499 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package pages
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
"reichard.io/antholume/ngtemplates/components"
|
||||
)
|
||||
|
||||
func getNavigationLinkClass(isActive bool) string {
|
||||
defaultClass := "flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4"
|
||||
if isActive {
|
||||
return fmt.Sprintf("%s border-purple-500 dark:text-white", defaultClass)
|
||||
} else {
|
||||
return fmt.Sprintf("%s border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", defaultClass)
|
||||
}
|
||||
}
|
||||
|
||||
func navigation(settings common.Settings) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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\"> <span class=\"lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white\"></span> <span class=\"lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white\"></span> <span class=\"lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white\"></span><div id=\"menu\" class=\"fixed -ml-6 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-8 lg:pr-0\">AnthoLume</p></div><div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 = []any{getNavigationLinkClass(settings.Route == common.RouteHome)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"/\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.HomeSVG("size-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"mx-4 text-sm font-normal\">Home</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 = []any{getNavigationLinkClass(settings.Route == common.RouteDocuments)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<a href=\"/documents\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var4).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.DocumentSVG("size-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"mx-4 text-sm font-normal\">Documents</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 = []any{getNavigationLinkClass(settings.Route == common.RouteProgress)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"/progress\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var6).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.ActivitySVG("size-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"mx-4 text-sm font-normal\">Progress</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 = []any{getNavigationLinkClass(settings.Route == common.RouteActivity)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<a href=\"/activity\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.ActivitySVG("size-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"mx-4 text-sm font-normal\">Activity</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if settings.SearchEnabled {
|
||||
var templ_7745c5c3_Var10 = []any{getNavigationLinkClass(settings.Route == common.RouteSearch)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<a href=\"/search\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var10).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.SearchSVG("size-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<span class=\"mx-4 text-sm font-normal\">Search</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if settings.IsAdmin {
|
||||
var templ_7745c5c3_Var12 = []any{
|
||||
"flex flex-col gap-4 p-2 pl-6 my-2 transition-colors duration-200 border-l-4",
|
||||
templ.KV("dark:text-white border-purple-500", settings.Route.IsAdmin()),
|
||||
templ.KV("border-transparent text-gray-400", !settings.Route.IsAdmin()),
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 = []any{
|
||||
"flex justify-start w-full",
|
||||
templ.KV("text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", !settings.Route.IsAdmin()),
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<a href=\"/admin\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.SettingsSVG("size-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"mx-4 text-sm font-normal\">Admin</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if settings.Route.IsAdmin() {
|
||||
var templ_7745c5c3_Var16 = []any{
|
||||
"flex justify-start w-full",
|
||||
templ.KV("text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", settings.Route != common.RouteAdmin),
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<a href=\"/admin\" style=\"padding-left: 1.75em\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var16).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\"><span class=\"mx-4 text-sm font-normal\">General</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 = []any{
|
||||
"flex justify-start w-full",
|
||||
templ.KV("text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", settings.Route != common.RouteAdminImport),
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<a href=\"/admin/import\" style=\"padding-left: 1.75em\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var18).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"><span class=\"mx-4 text-sm font-normal\">Import</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 = []any{
|
||||
"flex justify-start w-full",
|
||||
templ.KV("text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", settings.Route != common.RouteAdminUsers),
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<a href=\"/admin/users\" style=\"padding-left: 1.75em\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var20).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"><span class=\"mx-4 text-sm font-normal\">Users</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 = []any{
|
||||
"flex justify-start w-full",
|
||||
templ.KV("text-gray-400 hover:text-gray-800 dark:hover:text-gray-100", settings.Route != common.RouteAdminLogs),
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<a href=\"/admin/logs\" style=\"padding-left: 1.75em\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var22).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\"><span class=\"mx-4 text-sm font-normal\">Logs</span></a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div><a class=\"flex flex-col gap-2 justify-center items-center p-6 w-full absolute bottom-0 text-black dark:text-white\" target=\"_blank\" href=\"https://gitea.va.reichard.io/evan/AnthoLume\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.GitSVG("h-5 text-black dark:text-white").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<span class=\"text-xs\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(settings.Version)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 135, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</span></a></div></div><h1 class=\"text-xl font-bold dark:text-white px-6 lg:ml-44\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(settings.Route.Name())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 140, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</h1><div class=\"relative flex items-center justify-end w-full p-4 space-x-4\"><a href=\"#\" class=\"relative block text-gray-800 dark:text-gray-200\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.UserSVG("size-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</a> <input type=\"checkbox\" id=\"user-dropdown-button\" class=\"hidden\"><div id=\"user-dropdown\" class=\"transition duration-200 z-20 absolute right-4 top-16 pt-4\"><div class=\"w-40 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5\"><div class=\"py-1\" role=\"menu\" aria-orientation=\"vertical\" aria-labelledby=\"options-menu\"><a href=\"/settings\" class=\"block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600\" role=\"menuitem\"><span class=\"flex flex-col\"><span>Settings</span></span></a> <a href=\"/local\" class=\"block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600\" role=\"menuitem\"><span class=\"flex flex-col\"><span>Offline</span></span></a> <a href=\"/logout\" class=\"block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600\" role=\"menuitem\"><span class=\"flex flex-col\"><span>Logout</span></span></a></div></div></div><label for=\"user-dropdown-button\"><div class=\"flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer\"><span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(settings.User)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 194, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</span> <span class=\"text-gray-800 dark:text-gray-200\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.DropdownSVG("size-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</span></div></label></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func layout(settings common.Settings, title string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var27 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var27 == nil {
|
||||
templ_7745c5c3_Var27 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover\"><meta name=\"apple-mobile-web-app-capable\" content=\"yes\"><meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\"><meta name=\"theme-color\" content=\"#F3F4F6\" media=\"(prefers-color-scheme: light)\"><meta name=\"theme-color\" content=\"#1F2937\" media=\"(prefers-color-scheme: dark)\"><title>AnthoLume - ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/pages/layout.templ`, Line: 227, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</title><link rel=\"manifest\" href=\"/manifest.json\"><link rel=\"stylesheet\" href=\"/assets/style.css\"><!-- Service Worker / Offline Cache Flush --><script src=\"/assets/lib/idb-keyval.min.js\"></script><script src=\"/assets/common.js\"></script><script src=\"/assets/index.js\"></script><style>\n\t/* ----------------------------- */\n\t/* -------- PWA Styling -------- */\n\t/* ----------------------------- */\n\thtml,\n\tbody {\n\t overscroll-behavior-y: none;\n\t margin: 0px;\n\t}\n\n\thtml {\n\t height: calc(100% + env(safe-area-inset-bottom));\n\t padding: env(safe-area-inset-top) env(safe-area-inset-right) 0\n\t env(safe-area-inset-left);\n\t}\n\n\tmain {\n\t height: calc(100dvh - 4rem - env(safe-area-inset-top));\n\t}\n\n\t#container {\n\t padding-bottom: calc(5em + env(safe-area-inset-bottom) * 2);\n\t}\n\n\t/* No Scrollbar - IE, Edge, Firefox */\n\t* {\n\t -ms-overflow-style: none;\n\t scrollbar-width: none;\n\t}\n\n\t/* No Scrollbar - WebKit */\n\t*::-webkit-scrollbar {\n\t display: none;\n\t}\n\n\t/* ----------------------------- */\n\t/* -------- CSS Button -------- */\n\t/* ----------------------------- */\n\t.css-button:checked + div {\n\t visibility: visible;\n\t opacity: 1;\n\t}\n\n\t.css-button + div {\n\t visibility: hidden;\n\t opacity: 0;\n\t}\n\n\t/* ----------------------------- */\n\t/* ------- User Dropdown ------- */\n\t/* ----------------------------- */\n\t#user-dropdown-button:checked + #user-dropdown {\n\t visibility: visible;\n\t opacity: 1;\n\t}\n\n\t#user-dropdown {\n\t visibility: hidden;\n\t opacity: 0;\n\t}\n\n\t/* ----------------------------- */\n\t/* ----- Mobile Navigation ----- */\n\t/* ----------------------------- */\n\t#mobile-nav-button span {\n\t transform-origin: 5px 0px;\n\t transition:\n\t transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1),\n\t background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1),\n\t opacity 0.55s ease;\n\t}\n\n\t#mobile-nav-button span:first-child {\n\t transform-origin: 0% 0%;\n\t}\n\n\t#mobile-nav-button span:nth-last-child(2) {\n\t transform-origin: 0% 100%;\n\t}\n\n\t#mobile-nav-button input:checked ~ span {\n\t opacity: 1;\n\t transform: rotate(45deg) translate(2px, -2px);\n\t}\n\n\t#mobile-nav-button input:checked ~ span:nth-last-child(3) {\n\t opacity: 0;\n\t transform: rotate(0deg) scale(0.2, 0.2);\n\t}\n\n\t#mobile-nav-button input:checked ~ span:nth-last-child(2) {\n\t transform: rotate(-45deg) translate(0, 6px);\n\t}\n\n\t#mobile-nav-button input:checked ~ div {\n\t transform: none;\n\t}\n\n\t@media (min-width: 1024px) {\n\t #mobile-nav-button input ~ div {\n\t transform: none;\n\t }\n\t}\n\n\t#menu {\n\t top: 0;\n\t padding-top: env(safe-area-inset-top);\n\t transform-origin: 0% 0%;\n\t transform: translate(-100%, 0);\n\t transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1);\n\t}\n\n\t@media (orientation: landscape) {\n\t #menu {\n\t transform: translate(\n\t calc(-1 * (env(safe-area-inset-left) + 100%)),\n\t 0\n\t );\n\t }\n\t}\n </style></head><body class=\"bg-gray-100 dark:bg-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = navigation(settings).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<main class=\"relative overflow-hidden\"><div id=\"container\" class=\"h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var27.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</div></main><div class=\"absolute right-4 bottom-4\"><!--\n\t<div class=\"w-72 p-4 bg-red-500 rounded-xl\">\n\t <span>User Deleted</span>\n\t</div>\n\t--></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
55
package-lock.json
generated
Normal file
55
package-lock.json
generated
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "antholume",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "antholume",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"prettier-plugin-go-template": "^0.0.15"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
|
||||
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-go-template": {
|
||||
"version": "0.0.15",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-go-template/-/prettier-plugin-go-template-0.0.15.tgz",
|
||||
"integrity": "sha512-WqU92E1NokWYNZ9mLE6ijoRg6LtIGdLMePt2C7UBDjXeDH9okcRI3zRqtnWR4s5AloiqyvZ66jNBAa9tmRY5EQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ulid": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prettier": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ulid": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz",
|
||||
"integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"ulid": "bin/cli.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
package.json
Normal file
7
package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "antholume",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"prettier-plugin-go-template": "^0.0.15"
|
||||
}
|
||||
}
|
||||
@@ -3,26 +3,23 @@ package search
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func parseAnnasArchiveDownloadURL(body io.ReadCloser) (string, error) {
|
||||
// Parse
|
||||
defer body.Close()
|
||||
doc, _ := goquery.NewDocumentFromReader(body)
|
||||
var commentRE = regexp.MustCompile(`(?s)<!--(.*?)-->`)
|
||||
|
||||
// Return Download URL
|
||||
downloadURL, exists := doc.Find("body > table > tbody > tr > td > a").Attr("href")
|
||||
if exists == false {
|
||||
return "", fmt.Errorf("Download URL not found")
|
||||
func searchAnnasArchive(query string) ([]SearchItem, error) {
|
||||
searchURL := "https://annas-archive.org/search?index=&q=%s&ext=epub&sort=&lang=en"
|
||||
url := fmt.Sprintf(searchURL, url.QueryEscape(query))
|
||||
body, err := getPage(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Possible Funky URL
|
||||
downloadURL = strings.ReplaceAll(downloadURL, "\\", "/")
|
||||
|
||||
return downloadURL, nil
|
||||
return parseAnnasArchive(body)
|
||||
}
|
||||
|
||||
func parseAnnasArchive(body io.ReadCloser) ([]SearchItem, error) {
|
||||
@@ -36,18 +33,20 @@ func parseAnnasArchive(body io.ReadCloser) ([]SearchItem, error) {
|
||||
// Normalize Results
|
||||
var allEntries []SearchItem
|
||||
doc.Find("form > div.w-full > div.w-full > div > div.justify-center").Each(func(ix int, rawBook *goquery.Selection) {
|
||||
rawBook = getAnnasArchiveBookSelection(rawBook)
|
||||
|
||||
// Parse Details
|
||||
details := rawBook.Find("div:nth-child(2) > div:nth-child(1)").Text()
|
||||
detailsSplit := strings.Split(details, ", ")
|
||||
|
||||
// Invalid Details
|
||||
if len(detailsSplit) < 3 {
|
||||
if len(detailsSplit) < 4 {
|
||||
return
|
||||
}
|
||||
|
||||
language := detailsSplit[0]
|
||||
fileType := detailsSplit[1]
|
||||
fileSize := detailsSplit[2]
|
||||
fileSize := detailsSplit[3]
|
||||
|
||||
// Get Title & Author
|
||||
title := rawBook.Find("h3").Text()
|
||||
@@ -73,3 +72,32 @@ func parseAnnasArchive(body io.ReadCloser) ([]SearchItem, error) {
|
||||
// Return Results
|
||||
return allEntries, nil
|
||||
}
|
||||
|
||||
// getAnnasArchiveBookSelection parses potentially commented out HTML. For some reason
|
||||
// Annas Archive comments out blocks "below the fold". They aren't rendered until you
|
||||
// scroll. This attempts to parse the commented out HTML.
|
||||
func getAnnasArchiveBookSelection(rawBook *goquery.Selection) *goquery.Selection {
|
||||
rawHTML, err := rawBook.Html()
|
||||
if err != nil {
|
||||
return rawBook
|
||||
}
|
||||
|
||||
strippedHTML := strings.TrimSpace(rawHTML)
|
||||
if !strings.HasPrefix(strippedHTML, "<!--") || !strings.HasSuffix(strippedHTML, "-->") {
|
||||
return rawBook
|
||||
}
|
||||
|
||||
allMatches := commentRE.FindAllStringSubmatch(strippedHTML, -1)
|
||||
if len(allMatches) != 1 || len(allMatches[0]) != 2 {
|
||||
return rawBook
|
||||
}
|
||||
|
||||
captureGroup := allMatches[0][1]
|
||||
docReader := strings.NewReader(captureGroup)
|
||||
doc, err := goquery.NewDocumentFromReader(docReader)
|
||||
if err != nil {
|
||||
return rawBook
|
||||
}
|
||||
|
||||
return doc.Selection
|
||||
}
|
||||
|
||||
69
search/downloaders.go
Normal file
69
search/downloaders.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func getLibGenDownloadURL(md5 string, _ Source) ([]string, error) {
|
||||
// Get Page
|
||||
body, err := getPage("http://libgen.li/ads.php?md5=" + md5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
// Parse
|
||||
doc, err := goquery.NewDocumentFromReader(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return Download URL
|
||||
downloadPath, exists := doc.Find("body > table > tbody > tr > td > a").Attr("href")
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("Download URL not found")
|
||||
}
|
||||
|
||||
// Possible Funky URL
|
||||
downloadPath = strings.ReplaceAll(downloadPath, "\\", "/")
|
||||
return []string{fmt.Sprintf("http://libgen.li/%s", downloadPath)}, nil
|
||||
}
|
||||
|
||||
func getLibraryDownloadURL(md5 string, source Source) ([]string, error) {
|
||||
// Derive Info URL
|
||||
var infoURL string
|
||||
switch source {
|
||||
case SOURCE_LIBGEN_FICTION, SOURCE_ANNAS_ARCHIVE:
|
||||
infoURL = "http://library.lol/fiction/" + md5
|
||||
case SOURCE_LIBGEN_NON_FICTION:
|
||||
infoURL = "http://library.lol/main/" + md5
|
||||
default:
|
||||
return nil, errors.New("invalid source")
|
||||
}
|
||||
|
||||
// Get Page
|
||||
body, err := getPage(infoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
// Parse
|
||||
doc, err := goquery.NewDocumentFromReader(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return Download URL
|
||||
// downloadURL, _ := doc.Find("#download [href*=cloudflare]").Attr("href")
|
||||
downloadURL, exists := doc.Find("#download h2 a").Attr("href")
|
||||
if !exists {
|
||||
return nil, errors.New("Download URL not found")
|
||||
}
|
||||
|
||||
return []string{downloadURL}, nil
|
||||
}
|
||||
@@ -3,12 +3,23 @@ package search
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func searchLibGenFiction(query string) ([]SearchItem, error) {
|
||||
searchURL := "https://libgen.is/fiction/?q=%s&language=English&format=epub"
|
||||
url := fmt.Sprintf(searchURL, url.QueryEscape(query))
|
||||
body, err := getPage(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseLibGenFiction(body)
|
||||
}
|
||||
|
||||
func parseLibGenFiction(body io.ReadCloser) ([]SearchItem, error) {
|
||||
// Parse
|
||||
defer body.Close()
|
||||
@@ -62,6 +73,16 @@ func parseLibGenFiction(body io.ReadCloser) ([]SearchItem, error) {
|
||||
return allEntries, nil
|
||||
}
|
||||
|
||||
func searchLibGenNonFiction(query string) ([]SearchItem, error) {
|
||||
searchURL := "https://libgen.is/search.php?req=%s"
|
||||
url := fmt.Sprintf(searchURL, url.QueryEscape(query))
|
||||
body, err := getPage(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseLibGenNonFiction(body)
|
||||
}
|
||||
|
||||
func parseLibGenNonFiction(body io.ReadCloser) ([]SearchItem, error) {
|
||||
// Parse
|
||||
defer body.Close()
|
||||
@@ -106,18 +127,3 @@ func parseLibGenNonFiction(body io.ReadCloser) ([]SearchItem, error) {
|
||||
// Return Results
|
||||
return allEntries, nil
|
||||
}
|
||||
|
||||
func parseLibGenDownloadURL(body io.ReadCloser) (string, error) {
|
||||
// Parse
|
||||
defer body.Close()
|
||||
doc, _ := goquery.NewDocumentFromReader(body)
|
||||
|
||||
// Return Download URL
|
||||
// downloadURL, _ := doc.Find("#download [href*=cloudflare]").Attr("href")
|
||||
downloadURL, exists := doc.Find("#download h2 a").Attr("href")
|
||||
if exists == false {
|
||||
return "", fmt.Errorf("Download URL not found")
|
||||
}
|
||||
|
||||
return downloadURL, nil
|
||||
}
|
||||
|
||||
22
search/progress.go
Normal file
22
search/progress.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package search
|
||||
|
||||
type writeCounter struct {
|
||||
Total int64
|
||||
Current int64
|
||||
ProgressFunction func(float32)
|
||||
}
|
||||
|
||||
func (wc *writeCounter) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
wc.Current += int64(n)
|
||||
wc.flushProgress()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (wc *writeCounter) flushProgress() {
|
||||
if wc.ProgressFunction == nil || wc.Total < 100000 {
|
||||
return
|
||||
}
|
||||
percentage := float32(wc.Current) * 100 / float32(wc.Total)
|
||||
wc.ProgressFunction(percentage)
|
||||
}
|
||||
183
search/search.go
183
search/search.go
@@ -2,17 +2,18 @@ package search
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/antholume/metadata"
|
||||
)
|
||||
|
||||
const userAgent string = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0"
|
||||
const userAgent string = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
|
||||
type Cadence string
|
||||
|
||||
@@ -21,13 +22,6 @@ const (
|
||||
CADENCE_TOP_MONTH Cadence = "m"
|
||||
)
|
||||
|
||||
type BookType int
|
||||
|
||||
const (
|
||||
BOOK_FICTION BookType = iota
|
||||
BOOK_NON_FICTION
|
||||
)
|
||||
|
||||
type Source string
|
||||
|
||||
const (
|
||||
@@ -47,120 +41,75 @@ type SearchItem struct {
|
||||
UploadDate string
|
||||
}
|
||||
|
||||
type sourceDef struct {
|
||||
searchURL string
|
||||
downloadURL string
|
||||
parseSearchFunc func(io.ReadCloser) ([]SearchItem, error)
|
||||
parseDownloadFunc func(io.ReadCloser) (string, error)
|
||||
type searchFunc func(query string) (searchResults []SearchItem, err error)
|
||||
type downloadFunc func(md5 string, source Source) (downloadURL []string, err error)
|
||||
|
||||
var searchDefs = map[Source]searchFunc{
|
||||
SOURCE_ANNAS_ARCHIVE: searchAnnasArchive,
|
||||
SOURCE_LIBGEN_FICTION: searchLibGenFiction,
|
||||
SOURCE_LIBGEN_NON_FICTION: searchLibGenNonFiction,
|
||||
}
|
||||
|
||||
var sourceDefs = map[Source]sourceDef{
|
||||
SOURCE_ANNAS_ARCHIVE: {
|
||||
searchURL: "https://annas-archive.org/search?index=&q=%s&ext=epub&sort=&lang=en",
|
||||
downloadURL: "http://libgen.li/ads.php?md5=%s",
|
||||
parseSearchFunc: parseAnnasArchive,
|
||||
parseDownloadFunc: parseAnnasArchiveDownloadURL,
|
||||
},
|
||||
SOURCE_LIBGEN_FICTION: {
|
||||
searchURL: "https://libgen.is/fiction/?q=%s&language=English&format=epub",
|
||||
downloadURL: "http://library.lol/fiction/%s",
|
||||
parseSearchFunc: parseLibGenFiction,
|
||||
parseDownloadFunc: parseLibGenDownloadURL,
|
||||
},
|
||||
SOURCE_LIBGEN_NON_FICTION: {
|
||||
searchURL: "https://libgen.is/search.php?req=%s",
|
||||
downloadURL: "http://library.lol/main/%s",
|
||||
parseSearchFunc: parseLibGenNonFiction,
|
||||
parseDownloadFunc: parseLibGenDownloadURL,
|
||||
},
|
||||
var downloadFuncs = []downloadFunc{
|
||||
getLibGenDownloadURL,
|
||||
getLibraryDownloadURL,
|
||||
}
|
||||
|
||||
func SearchBook(query string, source Source) ([]SearchItem, error) {
|
||||
def := sourceDefs[source]
|
||||
log.Debug("Source: ", def)
|
||||
url := fmt.Sprintf(def.searchURL, url.QueryEscape(query))
|
||||
body, err := getPage(url)
|
||||
searchFunc, found := searchDefs[source]
|
||||
if !found {
|
||||
return nil, fmt.Errorf("invalid source: %s", source)
|
||||
}
|
||||
log.Debug("Source: ", source)
|
||||
return searchFunc(query)
|
||||
}
|
||||
|
||||
func SaveBook(md5 string, source Source, progressFunc func(float32)) (string, *metadata.MetadataInfo, error) {
|
||||
for _, f := range downloadFuncs {
|
||||
downloadURLs, err := f(md5, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return def.parseSearchFunc(body)
|
||||
log.Error("failed to acquire download urls")
|
||||
continue
|
||||
}
|
||||
|
||||
func SaveBook(id string, source Source) (string, error) {
|
||||
def := sourceDefs[source]
|
||||
log.Debug("Source: ", def)
|
||||
url := fmt.Sprintf(def.downloadURL, id)
|
||||
|
||||
body, err := getPage(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bookURL, err := def.parseDownloadFunc(body)
|
||||
if err != nil {
|
||||
log.Error("Parse Download URL Error: ", err)
|
||||
return "", fmt.Errorf("Download Failure")
|
||||
}
|
||||
|
||||
// Create File
|
||||
tempFile, err := os.CreateTemp("", "book")
|
||||
if err != nil {
|
||||
log.Error("File Create Error: ", err)
|
||||
return "", fmt.Errorf("File Failure")
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
for _, bookURL := range downloadURLs {
|
||||
// Download File
|
||||
log.Info("Downloading Book: ", bookURL)
|
||||
resp, err := downloadBook(bookURL)
|
||||
fileName, err := downloadBook(bookURL, progressFunc)
|
||||
if err != nil {
|
||||
os.Remove(tempFile.Name())
|
||||
log.Error("Book URL API Failure: ", err)
|
||||
return "", fmt.Errorf("API Failure")
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Copy File to Disk
|
||||
log.Info("Saving Book")
|
||||
_, err = io.Copy(tempFile, resp.Body)
|
||||
// Get Metadata
|
||||
metadata, err := metadata.GetMetadata(fileName)
|
||||
if err != nil {
|
||||
os.Remove(tempFile.Name())
|
||||
log.Error("File Copy Error: ", err)
|
||||
return "", fmt.Errorf("File Failure")
|
||||
log.Error("Book Metadata Failure: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
return tempFile.Name(), nil
|
||||
return fileName, metadata, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetBookURL(id string, bookType BookType) (string, error) {
|
||||
// Derive Info URL
|
||||
var infoURL string
|
||||
if bookType == BOOK_FICTION {
|
||||
infoURL = "http://library.lol/fiction/" + id
|
||||
} else if bookType == BOOK_NON_FICTION {
|
||||
infoURL = "http://library.lol/main/" + id
|
||||
}
|
||||
|
||||
// Parse & Derive Download URL
|
||||
body, err := getPage(infoURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// downloadURL := parseLibGenDownloadURL(body)
|
||||
return parseLibGenDownloadURL(body)
|
||||
return "", nil, errors.New("failed to download book")
|
||||
}
|
||||
|
||||
func getPage(page string) (io.ReadCloser, error) {
|
||||
log.Debug("URL: ", page)
|
||||
|
||||
// Set 10s Timeout
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
client := http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
// Get Page
|
||||
resp, err := client.Get(page)
|
||||
// Start Request
|
||||
req, err := http.NewRequest("GET", page, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
// Do Request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -169,20 +118,46 @@ func getPage(page string) (io.ReadCloser, error) {
|
||||
return resp.Body, err
|
||||
}
|
||||
|
||||
func downloadBook(bookURL string) (*http.Response, error) {
|
||||
func downloadBook(bookURL string, progressFunc func(float32)) (string, error) {
|
||||
log.Debug("URL: ", bookURL)
|
||||
|
||||
// Allow Insecure
|
||||
client := &http.Client{Transport: &http.Transport{
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}}
|
||||
},
|
||||
}
|
||||
|
||||
// Start Request
|
||||
req, err := http.NewRequest("GET", bookURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Set UserAgent
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
return client.Do(req)
|
||||
// Perform API Request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create File
|
||||
tempFile, err := os.CreateTemp("", "book")
|
||||
if err != nil {
|
||||
log.Error("File Create Error: ", err)
|
||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
// Copy File to Disk
|
||||
log.Info("Saving Book")
|
||||
counter := &writeCounter{Total: resp.ContentLength, ProgressFunction: progressFunc}
|
||||
_, err = io.Copy(tempFile, io.TeeReader(resp.Body, counter))
|
||||
if err != nil {
|
||||
os.Remove(tempFile.Name())
|
||||
log.Error("File Copy Error: ", err)
|
||||
return "", fmt.Errorf("failed to copy response to temp file: %w", err)
|
||||
}
|
||||
|
||||
return tempFile.Name(), nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
|
||||
let
|
||||
unstable = import <nixpkgs-unstable> { };
|
||||
in
|
||||
pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
go
|
||||
nodejs
|
||||
nodePackages.tailwindcss
|
||||
unstable.templ
|
||||
python311Packages.grip
|
||||
];
|
||||
shellHook = ''
|
||||
|
||||
@@ -19,6 +19,10 @@ sql:
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "documents.basepath"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "documents.coverfile"
|
||||
go_type:
|
||||
type: "string"
|
||||
@@ -120,7 +124,7 @@ sql:
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "users.time_offset"
|
||||
- column: "users.timezone"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./ngtemplates/**/*.templ",
|
||||
"./templates/**/*.{tmpl,html,htm,svg}",
|
||||
"./assets/local/*.{html,htm,svg,js}",
|
||||
"./assets/reader/*.{html,htm,svg,js}",
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent" />
|
||||
<meta name="theme-color"
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#F3F4F6"
|
||||
media="(prefers-color-scheme: light)" />
|
||||
<meta name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#1F2937"
|
||||
media="(prefers-color-scheme: dark)" />
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<title>AnthoLume - {{ block "title" . }}{{ end }}</title>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
@@ -32,7 +40,8 @@
|
||||
|
||||
html {
|
||||
height: calc(100% + env(safe-area-inset-bottom));
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0
|
||||
env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
main {
|
||||
@@ -54,6 +63,19 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ----------------------------- */
|
||||
/* -------- CSS Button -------- */
|
||||
/* ----------------------------- */
|
||||
.css-button:checked + div {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.css-button + div {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ----------------------------- */
|
||||
/* ------- User Dropdown ------- */
|
||||
/* ----------------------------- */
|
||||
@@ -72,8 +94,9 @@
|
||||
/* ----------------------------- */
|
||||
#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),
|
||||
transition:
|
||||
transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1),
|
||||
background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1),
|
||||
opacity 0.55s ease;
|
||||
}
|
||||
|
||||
@@ -114,12 +137,15 @@
|
||||
padding-top: env(safe-area-inset-top);
|
||||
transform-origin: 0% 0%;
|
||||
transform: translate(-100%, 0);
|
||||
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0);
|
||||
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1);
|
||||
}
|
||||
|
||||
@media (orientation: landscape) {
|
||||
#menu {
|
||||
transform: translate(calc(-1 * (env(safe-area-inset-left) + 100%)), 0);
|
||||
transform: translate(
|
||||
calc(-1 * (env(safe-area-inset-left) + 100%)),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -127,87 +153,161 @@
|
||||
<body class="bg-gray-100 dark:bg-gray-800">
|
||||
<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" />
|
||||
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white"></span>
|
||||
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
||||
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
||||
<div id="menu"
|
||||
class="fixed -ml-6 h-full w-56 lg:w-48 bg-white dark:bg-gray-700 shadow-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0"
|
||||
/>
|
||||
<span
|
||||
class="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white"
|
||||
></span>
|
||||
<span
|
||||
class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"
|
||||
></span>
|
||||
<span
|
||||
class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"
|
||||
></span>
|
||||
<div
|
||||
id="menu"
|
||||
class="fixed -ml-6 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-8 lg:pr-0">AnthoLume</p>
|
||||
<p
|
||||
class="text-xl font-bold dark:text-white text-right my-auto pr-8 lg:pr-0"
|
||||
>
|
||||
AnthoLume
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{{ $default := "flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4" }}
|
||||
{{ $inactive := "border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100" }}
|
||||
{{ $active := "border-purple-500 dark:text-white" }}
|
||||
<a class="{{ $default }} {{ if eq .RouteName "home" }}{{ $active }}{{ else if true }}{{ $inactive }}{{ end }}"
|
||||
href="/">
|
||||
<a
|
||||
class="{{ $default }} {{ if eq .RouteName "home" }}
|
||||
{{ $active }}
|
||||
{{ else if true }}
|
||||
{{ $inactive }}
|
||||
{{ end }}"
|
||||
href="/"
|
||||
>
|
||||
{{ template "svg/home" (dict "Size" 20) }}
|
||||
<span class="mx-4 text-sm font-normal">Home</span>
|
||||
</a>
|
||||
<a class="{{ $default }} {{ if eq .RouteName "documents" }}{{ $active }}{{ else if true }}{{ $inactive }}{{ end }}"
|
||||
href="/documents">
|
||||
<a
|
||||
class="{{ $default }} {{ if eq .RouteName "documents" }}
|
||||
{{ $active }}
|
||||
{{ else if true }}
|
||||
{{ $inactive }}
|
||||
{{ end }}"
|
||||
href="/documents"
|
||||
>
|
||||
{{ template "svg/documents" (dict "Size" 20) }}
|
||||
<span class="mx-4 text-sm font-normal">Documents</span>
|
||||
</a>
|
||||
<a class="{{ $default }} {{ if eq .RouteName "progress" }}{{ $active }}{{ else if true }}{{ $inactive }}{{ end }}"
|
||||
href="/progress">
|
||||
<a
|
||||
class="{{ $default }} {{ if eq .RouteName "progress" }}
|
||||
{{ $active }}
|
||||
{{ else if true }}
|
||||
{{ $inactive }}
|
||||
{{ end }}"
|
||||
href="/progress"
|
||||
>
|
||||
{{ template "svg/activity" (dict "Size" 20) }}
|
||||
<span class="mx-4 text-sm font-normal">Progress</span>
|
||||
</a>
|
||||
<a class="{{ $default }} {{ if eq .RouteName "activity" }}{{ $active }}{{ else if true }}{{ $inactive }}{{ end }}"
|
||||
href="/activity">
|
||||
<a
|
||||
class="{{ $default }} {{ if eq .RouteName "activity" }}
|
||||
{{ $active }}
|
||||
{{ else if true }}
|
||||
{{ $inactive }}
|
||||
{{ end }}"
|
||||
href="/activity"
|
||||
>
|
||||
{{ template "svg/activity" (dict "Size" 20) }}
|
||||
<span class="mx-4 text-sm font-normal">Activity</span>
|
||||
</a>
|
||||
{{ if .Config.SearchEnabled }}
|
||||
<a class="{{ $default }} {{ if eq .RouteName "search" }}{{ $active }}{{ else if true }}{{ $inactive }}{{ end }}"
|
||||
href="/search">
|
||||
<a
|
||||
class="{{ $default }} {{ if eq .RouteName "search" }}
|
||||
{{ $active }}
|
||||
{{ else if true }}
|
||||
{{ $inactive }}
|
||||
{{ end }}"
|
||||
href="/search"
|
||||
>
|
||||
{{ template "svg/search" (dict "Size" 20) }}
|
||||
<span class="mx-4 text-sm font-normal">Search</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ if .Authorization.IsAdmin }}
|
||||
<div class="flex flex-col gap-4 p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{ if hasPrefix .RouteName "admin" }}dark:text-white border-purple-500{{ else if true }}border-transparent text-gray-400{{ end }}">
|
||||
<a href="/admin"
|
||||
class="flex justify-start w-full {{ if not (hasPrefix .RouteName "admin") }}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{ end }}">
|
||||
<div
|
||||
class="flex flex-col gap-4 p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{ if hasPrefix .RouteName "admin" }}
|
||||
dark:text-white border-purple-500
|
||||
{{ else if true }}
|
||||
border-transparent text-gray-400
|
||||
{{ end }}"
|
||||
>
|
||||
<a
|
||||
href="/admin"
|
||||
class="flex justify-start w-full {{ if not (hasPrefix .RouteName "admin") }}
|
||||
text-gray-400 hover:text-gray-800 dark:hover:text-gray-100
|
||||
{{ end }}"
|
||||
>
|
||||
{{ template "svg/settings" (dict "Size" 20) }}
|
||||
<span class="mx-4 text-sm font-normal">Admin</span>
|
||||
</a>
|
||||
{{ if hasPrefix .RouteName "admin" }}
|
||||
<a href="/admin"
|
||||
<a
|
||||
href="/admin"
|
||||
style="padding-left: 1.75em"
|
||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin") }}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{ end }}">
|
||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin") }}
|
||||
text-gray-400 hover:text-gray-800 dark:hover:text-gray-100
|
||||
{{ end }}"
|
||||
>
|
||||
<span class="mx-4 text-sm font-normal">General</span>
|
||||
</a>
|
||||
<a href="/admin/import"
|
||||
<a
|
||||
href="/admin/import"
|
||||
style="padding-left: 1.75em"
|
||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin-import") }}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{ end }}">
|
||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin-import") }}
|
||||
text-gray-400 hover:text-gray-800 dark:hover:text-gray-100
|
||||
{{ end }}"
|
||||
>
|
||||
<span class="mx-4 text-sm font-normal">Import</span>
|
||||
</a>
|
||||
<a href="/admin/users"
|
||||
<a
|
||||
href="/admin/users"
|
||||
style="padding-left: 1.75em"
|
||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin-users") }}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{ end }}">
|
||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin-users") }}
|
||||
text-gray-400 hover:text-gray-800 dark:hover:text-gray-100
|
||||
{{ end }}"
|
||||
>
|
||||
<span class="mx-4 text-sm font-normal">Users</span>
|
||||
</a>
|
||||
<a href="/admin/logs"
|
||||
<a
|
||||
href="/admin/logs"
|
||||
style="padding-left: 1.75em"
|
||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin-logs") }}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{ end }}">
|
||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin-logs") }}
|
||||
text-gray-400 hover:text-gray-800 dark:hover:text-gray-100
|
||||
{{ end }}"
|
||||
>
|
||||
<span class="mx-4 text-sm font-normal">Logs</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<a class="flex flex-col gap-2 justify-center items-center p-6 w-full absolute bottom-0 text-black dark:text-white"
|
||||
<a
|
||||
class="flex flex-col gap-2 justify-center items-center p-6 w-full absolute bottom-0 text-black dark:text-white"
|
||||
target="_blank"
|
||||
href="https://gitea.va.reichard.io/evan/AnthoLume">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
href="https://gitea.va.reichard.io/evan/AnthoLume"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-black dark:text-white"
|
||||
height="20"
|
||||
viewBox="0 0 219 92"
|
||||
fill="currentColor">
|
||||
fill="currentColor"
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path d="M159 .79h25V69h-25Zm0 0" />
|
||||
@@ -219,49 +319,77 @@
|
||||
<path d="M0 .79h92V92H0Zm0 0" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path style="stroke: none; fill-rule: nonzero; fill-opacity: 1" d="M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61" />
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61"
|
||||
/>
|
||||
<g clip-path="url(#a)">
|
||||
<path style="stroke: none; fill-rule: nonzero; fill-opacity: 1" d="M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805" />
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#b)">
|
||||
<path style="stroke: none; fill-rule: nonzero; fill-opacity: 1" d="M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66" />
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#c)">
|
||||
<path style="stroke: none; fill-rule: nonzero; fill-opacity: 1" d="M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305" />
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<span class="text-xs">{{ .Config.Version }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold dark:text-white px-6 lg:ml-44">{{ block "header" . }}{{ end }}</h1>
|
||||
<h1 class="text-xl font-bold dark:text-white px-6 lg:ml-44">
|
||||
{{ block "header" . }}{{ end }}
|
||||
</h1>
|
||||
<div class="relative flex items-center justify-end w-full p-4 space-x-4">
|
||||
<a href="#" class="relative block text-gray-800 dark:text-gray-200">{{ template "svg/user" (dict "Size" 20) }}</a>
|
||||
<a href="#" class="relative block text-gray-800 dark:text-gray-200"
|
||||
>{{ template "svg/user" (dict "Size" 20) }}</a
|
||||
>
|
||||
<input type="checkbox" id="user-dropdown-button" class="hidden" />
|
||||
<div id="user-dropdown"
|
||||
class="transition duration-200 z-20 absolute right-4 top-16 pt-4">
|
||||
<div class="w-40 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5">
|
||||
<div class="py-1"
|
||||
<div
|
||||
id="user-dropdown"
|
||||
class="transition duration-200 z-20 absolute right-4 top-16 pt-4"
|
||||
>
|
||||
<div
|
||||
class="w-40 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div
|
||||
class="py-1"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="options-menu">
|
||||
<a href="/settings"
|
||||
aria-labelledby="options-menu"
|
||||
>
|
||||
<a
|
||||
href="/settings"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||
role="menuitem">
|
||||
role="menuitem"
|
||||
>
|
||||
<span class="flex flex-col">
|
||||
<span>Settings</span>
|
||||
</span>
|
||||
</a>
|
||||
<a href="/local"
|
||||
<a
|
||||
href="/local"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||
role="menuitem">
|
||||
role="menuitem"
|
||||
>
|
||||
<span class="flex flex-col">
|
||||
<span>Offline</span>
|
||||
</span>
|
||||
</a>
|
||||
<a href="/logout"
|
||||
<a
|
||||
href="/logout"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||
role="menuitem">
|
||||
role="menuitem"
|
||||
>
|
||||
<span class="flex flex-col">
|
||||
<span>Logout</span>
|
||||
</span>
|
||||
@@ -270,15 +398,32 @@
|
||||
</div>
|
||||
</div>
|
||||
<label for="user-dropdown-button">
|
||||
<div class="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer">
|
||||
<div
|
||||
class="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer"
|
||||
>
|
||||
<span>{{ .Authorization.UserName }}</span>
|
||||
<span class="text-gray-800 dark:text-gray-200">{{ template "svg/dropdown" (dict "Size" 20) }}</span>
|
||||
<span class="text-gray-800 dark:text-gray-200"
|
||||
>{{ template "svg/dropdown" (dict "Size" 20) }}</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<main class="relative overflow-hidden">
|
||||
<div id="container" class="h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48">{{ block "content" . }}{{ end }}</div>
|
||||
<div
|
||||
id="container"
|
||||
class="h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48"
|
||||
>
|
||||
{{ block "content" . }}{{ end }}
|
||||
</div>
|
||||
</main>
|
||||
<div class="absolute right-4 bottom-4">
|
||||
{{ block "notifications" . }}{{ end }}
|
||||
<!--
|
||||
<div class="w-72 p-4 bg-red-500 rounded-xl">
|
||||
<span>User Deleted</span>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
28
templates/components/activity.tmpl
Normal file
28
templates/components/activity.tmpl
Normal file
@@ -0,0 +1,28 @@
|
||||
{{ template "base" . }}
|
||||
{{ define "title" }}Activity{{ end }}
|
||||
{{ define "header" }}<a href="./activity">Activity</a>{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<!-- Table Component - Utilizes Template "table-cell" -->
|
||||
{{ template "component/table" (dict
|
||||
"Columns" (slice "Document" "Time" "Duration" "Percent")
|
||||
"Keys" (slice "Document" "StartTime" "Duration" "EndPercentage")
|
||||
"Rows" .Data
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<!-- Table Cell Definition -->
|
||||
{{ define "table-cell" }}
|
||||
{{ if eq .Name "Document" }}
|
||||
<a href="./documents/{{ .Data.DocumentID }}"
|
||||
>{{ .Data.Author }} - {{ .Data.Title }}</a
|
||||
>
|
||||
{{ else if eq .Name "EndPercentage" }}
|
||||
{{ index (fields .Data) .Name }}%
|
||||
{{ else }}
|
||||
{{ index (fields .Data) .Name }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
21
templates/components/button.tmpl
Normal file
21
templates/components/button.tmpl
Normal file
@@ -0,0 +1,21 @@
|
||||
<!-- Variant -->
|
||||
{{ $baseClass := "transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white" }}
|
||||
{{ if eq .Variant "Secondary" }}
|
||||
{{ $baseClass = printf "bg-black shadow-md hover:text-black hover:bg-white %s" $baseClass }}
|
||||
{{ else }}
|
||||
{{ $baseClass = printf "bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100 %s" $baseClass }}
|
||||
{{ end }}
|
||||
<!-- Type -->
|
||||
{{ if eq .Type "Link" }}
|
||||
<a href="{{ .URL }}" class="text-center {{ $baseClass }}" type="submit"
|
||||
>{{ .Title }}</a
|
||||
>
|
||||
{{ else }}
|
||||
<button
|
||||
class="{{ $baseClass }}"
|
||||
type="submit"
|
||||
{{ if .FormName }}form="{{ .FormName }}"{{ end }}
|
||||
>
|
||||
{{ .Title }}
|
||||
</button>
|
||||
{{ end }}
|
||||
50
templates/components/document-card.tmpl
Normal file
50
templates/components/document-card.tmpl
Normal file
@@ -0,0 +1,50 @@
|
||||
<div class="w-full relative">
|
||||
<div
|
||||
class="flex gap-4 w-full h-full p-4 shadow-lg bg-white dark:bg-gray-700 rounded"
|
||||
>
|
||||
<div class="min-w-fit my-auto h-48 relative">
|
||||
<a href="./documents/{{ .ID }}">
|
||||
<img
|
||||
class="rounded object-cover h-full"
|
||||
src="./documents/{{ .ID }}/cover"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Title</p>
|
||||
<p class="font-medium">{{ or .Title "Unknown" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Author</p>
|
||||
<p class="font-medium">{{ or .Author "Unknown" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Progress</p>
|
||||
<p class="font-medium">{{ .Percentage }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Time Read</p>
|
||||
<p class="font-medium">{{ niceSeconds .TotalTimeSeconds }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<a href="./activity?document={{ .ID }}">{{ template "svg/activity" }}</a>
|
||||
{{ if .Filepath }}
|
||||
<a href="./documents/{{ .ID }}/file">{{ template "svg/download" }}</a>
|
||||
{{ else }}
|
||||
{{ template "svg/download" (dict "Disabled" true) }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,13 @@
|
||||
<div class="absolute -translate-y-1/2 p-4 m-auto bg-gray-700 dark:bg-gray-300 rounded-lg shadow w-full text-black dark:text-white">
|
||||
<span class="inline-flex gap-2 items-center font-medium text-xs inline-block py-1 px-2 uppercase rounded-full {{ if .Error }} bg-red-500 {{ else if true }} bg-green-600 {{ end }}">
|
||||
<div
|
||||
class="absolute -translate-y-1/2 p-4 m-auto bg-gray-700 dark:bg-gray-300 rounded-lg shadow w-full text-black dark:text-white"
|
||||
>
|
||||
<span
|
||||
class="inline-flex gap-2 items-center font-medium text-xs inline-block py-1 px-2 uppercase rounded-full {{ if .Error }}
|
||||
bg-red-500
|
||||
{{ else if true }}
|
||||
bg-green-600
|
||||
{{ end }}"
|
||||
>
|
||||
{{ if and (ne .Progress 100) (not .Error) }}
|
||||
{{ template "svg/loading" (dict "Size" 16) }}
|
||||
{{ end }}
|
||||
@@ -8,15 +16,27 @@
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
<div class="relative w-full h-4 bg-gray-300 dark:bg-gray-700 rounded-full">
|
||||
{{ if .Error }}
|
||||
<div class="absolute h-full bg-red-500 rounded-full" style="width: 100%"></div>
|
||||
<p class="absolute w-full h-full font-bold text-center text-xs">ERROR</p>
|
||||
<div
|
||||
class="absolute h-full bg-red-500 rounded-full"
|
||||
style="width: 100%"
|
||||
></div>
|
||||
<p class="absolute w-full h-full font-bold text-center text-xs">
|
||||
ERROR
|
||||
</p>
|
||||
{{ else }}
|
||||
<div class="absolute h-full bg-green-600 rounded-full"
|
||||
style="width: {{ .Progress }}%"></div>
|
||||
<p class="absolute w-full h-full font-bold text-center text-xs">{{ .Progress }}%</p>
|
||||
<div
|
||||
class="absolute h-full bg-green-600 rounded-full"
|
||||
style="width: {{ .Progress }}%"
|
||||
></div>
|
||||
<p class="absolute w-full h-full font-bold text-center text-xs">
|
||||
{{ .Progress }}%
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
<a href="{{ .ButtonHref }}"
|
||||
class="w-full text-center font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100">{{ .ButtonText }}</a>
|
||||
<a
|
||||
href="{{ .ButtonHref }}"
|
||||
class="w-full text-center font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
>{{ .ButtonText }}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
12
templates/components/info-card.tmpl
Normal file
12
templates/components/info-card.tmpl
Normal file
@@ -0,0 +1,12 @@
|
||||
{{ if .Link }}<a href="{{ .Link }}" {{ else }} <div {{ end }}class="w-full">
|
||||
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<p class="text-2xl font-bold text-black dark:text-white">{{ .Size }}</p>
|
||||
<p class="text-sm text-gray-400">{{ .Title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .Link }}
|
||||
</a>
|
||||
{{ else }}
|
||||
</div>
|
||||
{{ end }}
|
||||
32
templates/components/key-val-edit.tmpl
Normal file
32
templates/components/key-val-edit.tmpl
Normal file
@@ -0,0 +1,32 @@
|
||||
<div class="relative">
|
||||
<div class="text-gray-500 inline-flex gap-2 relative">
|
||||
<p>{{ .Title }}</p>
|
||||
<label class="my-auto cursor-pointer" for="edit-{{ .FormValue }}-button">
|
||||
{{ template "svg/edit" (dict "Size" 18) }}
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="edit-{{ .FormValue }}-button"
|
||||
class="hidden css-button"
|
||||
/>
|
||||
<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="{{ .URL }}"
|
||||
class="flex flex-col gap-2 text-black dark:text-white text-sm"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="{{ .FormValue }}"
|
||||
name="{{ .FormValue }}"
|
||||
value="{{ or .Value "N/A" }}"
|
||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
{{ template "component/button" (dict "Title" "Save") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-medium text-lg">{{ or .Value "N/A" }}</p>
|
||||
</div>
|
||||
147
templates/components/metadata.tmpl
Normal file
147
templates/components/metadata.tmpl
Normal file
@@ -0,0 +1,147 @@
|
||||
{{ if .Error }}
|
||||
<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>
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Back to Document"
|
||||
"Type" "Link"
|
||||
"URL" (printf "/documents/%s" .ID)
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ 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="/documents/{{ .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.ID }}?fife=w480-h690"
|
||||
/>
|
||||
</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.ID }}"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex justify-end">
|
||||
<div class="flex gap-4 m-4 w-48">
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Cancel"
|
||||
"Type" "Link"
|
||||
"URL" (printf "/documents/%s" .ID)
|
||||
)
|
||||
}}
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Save"
|
||||
"FormName" "metadata-save"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
54
templates/components/streak-card.tmpl
Normal file
54
templates/components/streak-card.tmpl
Normal file
@@ -0,0 +1,54 @@
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="relative w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded"
|
||||
>
|
||||
<p
|
||||
class="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500"
|
||||
>
|
||||
{{ if eq .Window "WEEK" }}
|
||||
Weekly Read Streak
|
||||
{{ else }}
|
||||
Daily Read Streak
|
||||
{{ end }}
|
||||
</p>
|
||||
<div class="flex items-end my-6 space-x-2">
|
||||
<p class="text-5xl font-bold text-black dark:text-white">
|
||||
{{ .CurrentStreak }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="dark:text-white">
|
||||
<div
|
||||
class="flex items-center justify-between pb-2 mb-2 text-sm border-b border-gray-200"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
{{ if eq .Window "WEEK" }}
|
||||
Current Weekly Streak
|
||||
{{ else }}
|
||||
Current Daily Streak
|
||||
{{ end }}
|
||||
</p>
|
||||
<div class="flex items-end text-sm text-gray-400">
|
||||
{{ .CurrentStreakStartDate }} ➞ {{ .CurrentStreakEndDate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{{ .CurrentStreak }}</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between pb-2 mb-2 text-sm">
|
||||
<div>
|
||||
<p>
|
||||
{{ if eq .Window "WEEK" }}
|
||||
Best Weekly Streak
|
||||
{{ else }}
|
||||
Best Daily Streak
|
||||
{{ end }}
|
||||
</p>
|
||||
<div class="flex items-end text-sm text-gray-400">
|
||||
{{ .MaxStreakStartDate }} ➞ {{ .MaxStreakEndDate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{{ .MaxStreak }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
32
templates/components/table.tmpl
Normal file
32
templates/components/table.tmpl
Normal file
@@ -0,0 +1,32 @@
|
||||
{{ $rows := .Rows }}
|
||||
{{ $cols := .Columns }}
|
||||
{{ $keys := .Keys }}
|
||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||
<thead class="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
{{ range $col := $cols }}
|
||||
<th
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
{{ $col }}
|
||||
</th>
|
||||
{{ end }}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-black dark:text-white">
|
||||
{{ if not $rows }}
|
||||
<tr>
|
||||
<td class="text-center p-3" colspan="4">No Results</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range $row := $rows }}
|
||||
<tr>
|
||||
{{ range $key := $keys }}
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
{{ template "table-cell" (dict "Data" $row "Name" $key ) }}
|
||||
</td>
|
||||
{{ end }}
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -4,41 +4,25 @@
|
||||
{{ define "content" }}
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||
<thead class="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Document</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Time</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Duration</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Percent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-black dark:text-white">
|
||||
{{ if not .Data }}
|
||||
<tr>
|
||||
<td class="text-center p-3" colspan="4">No Results</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range $activity := .Data }}
|
||||
<tr>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<a href="./documents/{{ $activity.DocumentID }}">{{ $activity.Author }} - {{ $activity.Title }}
|
||||
</p>
|
||||
</a>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $activity.StartTime }}</p>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $activity.Duration }}</p>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $activity.EndPercentage }}%</p>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Table Component - Utilizes Template "table-cell" -->
|
||||
{{ template "component/table" (dict
|
||||
"Columns" (slice "Document" "Time" "Duration" "Percent")
|
||||
"Keys" (slice "Document" "StartTime" "Duration" "EndPercentage")
|
||||
"Rows" .Data
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<!-- Table Cell Definition -->
|
||||
{{ define "table-cell" }}
|
||||
{{ if eq .Name "Document" }}
|
||||
<a href="./documents/{{ .Data.DocumentID }}"
|
||||
>{{ .Data.Author }} - {{ .Data.Title }}</a
|
||||
>
|
||||
{{ else if eq .Name "EndPercentage" }}
|
||||
{{ index (fields .Data) .Name }}%
|
||||
{{ else }}
|
||||
{{ index (fields .Data) .Name }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
64
templates/pages/admin-import-results.tmpl
Normal file
64
templates/pages/admin-import-results.tmpl
Normal file
@@ -0,0 +1,64 @@
|
||||
{{ template "base" . }}
|
||||
{{ define "title" }}Admin - Import Results{{ end }}
|
||||
{{ define "header" }}
|
||||
<a class="whitespace-pre" href="../admin">Admin - Import Results</a>
|
||||
{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<table
|
||||
class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm"
|
||||
>
|
||||
<thead class="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Document
|
||||
</th>
|
||||
<th
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Error
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-black dark:text-white">
|
||||
{{ if not .Data }}
|
||||
<tr>
|
||||
<td class="text-center p-3" colspan="4">No Results</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range $result := .Data }}
|
||||
<tr>
|
||||
<td
|
||||
class="p-3 border-b border-gray-200 grid"
|
||||
style="grid-template-columns: 4rem auto"
|
||||
>
|
||||
<span class="text-gray-800 dark:text-gray-400">Name:</span>
|
||||
{{ if (eq $result.ID "") }}
|
||||
<span>N/A</span>
|
||||
{{ else }}
|
||||
<a href="../documents/{{ $result.ID }}">{{ $result.Name }}</a>
|
||||
{{ end }}
|
||||
<span class="text-gray-800 dark:text-gray-400">File:</span>
|
||||
<span>{{ $result.Path }}</span>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $result.Status }}</p>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $result.Error }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,52 +1,80 @@
|
||||
{{ template "base" . }}
|
||||
{{ define "title" }}Admin - Import{{ end }}
|
||||
{{ define "header" }}<a class="whitespace-pre" href="../admin">Admin - Import</a>{{ end }}
|
||||
{{ define "header" }}
|
||||
<a class="whitespace-pre" href="../admin">Admin - Import</a>
|
||||
{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
{{ if .SelectedDirectory }}
|
||||
<div class="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||
<p class="text-lg font-semibold text-gray-500">Selected Import Directory</p>
|
||||
<div
|
||||
class="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<p class="text-lg font-semibold text-gray-500">
|
||||
Selected Import Directory
|
||||
</p>
|
||||
<form class="flex gap-4 flex-col" action="./import" method="POST">
|
||||
<input type="text"
|
||||
<input
|
||||
type="text"
|
||||
name="directory"
|
||||
value="{{ .SelectedDirectory }}"
|
||||
class="hidden" />
|
||||
class="hidden"
|
||||
/>
|
||||
<div class="flex justify-between gap-4 w-full">
|
||||
<div class="flex gap-4 items-center">
|
||||
<span>{{ template "svg/import" }}</span>
|
||||
<p class="font-medium text-lg break-all">{{ .SelectedDirectory }}</p>
|
||||
<p class="font-medium text-lg break-all">
|
||||
{{ .SelectedDirectory }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-around gap-2 mr-4">
|
||||
<div class="inline-flex gap-2 items-center">
|
||||
<input checked type="radio" id="copy" name="type" value="COPY" />
|
||||
<label for="copy">Copy</label>
|
||||
</div>
|
||||
<div class="inline-flex gap-2 items-center">
|
||||
<input type="radio" id="direct" name="type" value="DIRECT" />
|
||||
<input
|
||||
checked
|
||||
type="radio"
|
||||
id="direct"
|
||||
name="type"
|
||||
value="DIRECT"
|
||||
/>
|
||||
<label for="direct">Direct</label>
|
||||
</div>
|
||||
<div class="inline-flex gap-2 items-center">
|
||||
<input type="radio" id="copy" name="type" value="COPY" />
|
||||
<label for="copy">Copy</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2">
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
|
||||
>
|
||||
<span class="w-full">Import Directory</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if not .SelectedDirectory }}
|
||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||
<table
|
||||
class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm"
|
||||
>
|
||||
<thead class="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th class="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 w-12"></th>
|
||||
<th class="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 break-all">{{ .CurrentPath }}</th>
|
||||
<th
|
||||
class="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 w-12"
|
||||
></th>
|
||||
<th
|
||||
class="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 break-all"
|
||||
>
|
||||
{{ .CurrentPath }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-black dark:text-white">
|
||||
{{ if not (eq .CurrentPath "/") }}
|
||||
<tr>
|
||||
<td class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400"></td>
|
||||
<td
|
||||
class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400"
|
||||
></td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<a href="./import?directory={{ $.CurrentPath }}/../">
|
||||
<p>../</p>
|
||||
@@ -61,8 +89,12 @@
|
||||
{{ end }}
|
||||
{{ range $item := .Data }}
|
||||
<tr>
|
||||
<td class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400">
|
||||
<a href="./import?select={{ $.CurrentPath }}/{{ $item }}">{{ template "svg/import" }}</a>
|
||||
<td
|
||||
class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400"
|
||||
>
|
||||
<a href="./import?select={{ $.CurrentPath }}/{{ $item }}"
|
||||
>{{ template "svg/import" }}</a
|
||||
>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<a href="./import?directory={{ $.CurrentPath }}/{{ $item }}">
|
||||
|
||||
@@ -1,32 +1,45 @@
|
||||
{{ template "base" . }}
|
||||
{{ define "title" }}Admin - Logs{{ end }}
|
||||
{{ define "header" }}<a class="whitespace-pre" href="../admin">Admin - Logs</a>{{ end }}
|
||||
{{ define "header" }}
|
||||
<a class="whitespace-pre" href="../admin">Admin - Logs</a>
|
||||
{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||
<div
|
||||
class="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<form class="flex gap-4 flex-col lg:flex-row" action="./logs" method="GET">
|
||||
<div class="flex flex-col w-full grow">
|
||||
<div class="flex relative">
|
||||
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
{{ template "svg/search2" (dict "Size" 15) }}
|
||||
</span>
|
||||
<input type="text"
|
||||
<input
|
||||
type="text"
|
||||
id="filter"
|
||||
name="filter"
|
||||
value="{{ .Filter }}"
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
placeholder="JQ Filter" />
|
||||
placeholder="JQ Filter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2">
|
||||
<span class="w-full">Filter</span>
|
||||
</button>
|
||||
<div class="lg:w-60">
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Filter"
|
||||
"Variant" "Secondary"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Required for iOS "Hover" Events (onclick) -->
|
||||
<div onclick
|
||||
<div
|
||||
onclick
|
||||
class="flex flex-col-reverse text-black dark:text-white w-full overflow-scroll"
|
||||
style="font-family: monospace">
|
||||
style="font-family: monospace"
|
||||
>
|
||||
{{ range $log := .Data }}
|
||||
<span class="whitespace-nowrap hover:whitespace-pre">{{ $log }}</span>
|
||||
{{ end }}
|
||||
|
||||
@@ -2,15 +2,40 @@
|
||||
{{ define "title" }}Admin - Users{{ end }}
|
||||
{{ define "header" }}<a class="whitespace-pre" href="../admin">Admin - Users</a>{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<div class="relative h-full overflow-x-auto">
|
||||
<input type="checkbox" id="add-button" class="hidden peer/add" />
|
||||
<div class="absolute top-10 left-10 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600 hidden peer-checked/add:block">
|
||||
<form method="POST"
|
||||
action="./users"
|
||||
class="flex flex-col gap-2 text-black dark:text-white text-sm">
|
||||
<input type="text"
|
||||
id="operation"
|
||||
name="operation"
|
||||
value="CREATE"
|
||||
class="hidden" />
|
||||
<input type="text"
|
||||
id="user"
|
||||
name="user"
|
||||
placeholder="User"
|
||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white" />
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
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 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="min-w-full overflow-scroll rounded shadow">
|
||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||
<thead class="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 w-12">
|
||||
{{ template "svg/add" }}
|
||||
<label class="cursor-pointer" for="add-button">{{ template "svg/add" }}</label>
|
||||
</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">User</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Password</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 text-center">
|
||||
Permissions
|
||||
</th>
|
||||
@@ -25,13 +50,75 @@
|
||||
{{ end }}
|
||||
{{ range $user := .Data }}
|
||||
<tr>
|
||||
<td class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400">{{ template "svg/delete" }}</td>
|
||||
<!-- User Deletion -->
|
||||
<td class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400 cursor-pointer relative">
|
||||
<label for="delete-{{ $user.ID }}-button" class="cursor-pointer">{{ template "svg/delete" }}</label>
|
||||
<input type="checkbox"
|
||||
id="delete-{{ $user.ID }}-button"
|
||||
class="hidden css-button" />
|
||||
<div class="absolute z-30 top-1.5 left-10 p-1.5 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="./users"
|
||||
class="text-black dark:text-white text-sm w-40">
|
||||
<input type="hidden" id="operation" name="operation" value="DELETE" />
|
||||
<input type="hidden" id="user" name="user" value="{{ $user.ID }}" />
|
||||
{{ template "component/button" (dict "Title" (printf "Delete (%s)" $user.ID )) }}
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
<!-- User ID -->
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $user.ID }}</p>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200 text-center min-w-40">
|
||||
<span class="px-2 py-1 rounded-md text-white dark:text-black {{ if $user.Admin }}bg-gray-800 dark:bg-gray-100{{ else }}bg-gray-400 dark:bg-gray-600 cursor-pointer{{ end }}">admin</span>
|
||||
<span class="px-2 py-1 rounded-md text-white dark:text-black {{ if $user.Admin }}bg-gray-400 dark:bg-gray-600 cursor-pointer{{ else }}bg-gray-800 dark:bg-gray-100{{ end }}">user</span>
|
||||
<!-- User Password Change -->
|
||||
<td class="border-b border-gray-200 relative px-3">
|
||||
<label for="edit-{{ $user.ID }}-button" class="cursor-pointer">
|
||||
<span class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit">Reset</span>
|
||||
</label>
|
||||
<input type="checkbox"
|
||||
id="edit-{{ $user.ID }}-button"
|
||||
class="hidden css-button" />
|
||||
<div class="absolute z-30 top-1 left-16 ml-2 p-1.5 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="./users"
|
||||
class="flex flex gap-2 text-black dark:text-white text-sm">
|
||||
<input type="hidden" id="operation" name="operation" value="UPDATE" />
|
||||
<input type="hidden" id="user" name="user" value="{{ $user.ID }}" />
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="{{ printf "Password (%s)" $user.ID }}"
|
||||
class="p-1.5 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 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit">Change</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
<!-- User Role -->
|
||||
<td class="flex gap-2 justify-center p-3 border-b border-gray-200 text-center min-w-40">
|
||||
<!-- Set Admin & User Styles -->
|
||||
{{ $adminStyle := "bg-gray-400 dark:bg-gray-600 cursor-pointer" }}
|
||||
{{ $userStyle := "bg-gray-400 dark:bg-gray-600 cursor-pointer" }}
|
||||
{{ if $user.Admin }}{{ $adminStyle = "bg-gray-800 dark:bg-gray-100 cursor-default" }}{{ end }}
|
||||
{{ if not $user.Admin }}{{ $userStyle = "bg-gray-800 dark:bg-gray-100 cursor-default" }}{{ end }}
|
||||
<form method="POST"
|
||||
action="./users"
|
||||
class="flex flex gap-2 text-black dark:text-white text-sm">
|
||||
<input type="hidden" id="operation" name="operation" value="UPDATE" />
|
||||
<input type="hidden" id="user" name="user" value="{{ $user.ID }}" />
|
||||
<input type="hidden" id="is_admin" name="is_admin" value="true" />
|
||||
<button {{ if $user.Admin }}type="button"{{ else }}type="submit"{{ end }} class="px-2 py-1 rounded-md text-white dark:text-black {{ $adminStyle }}">admin
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST"
|
||||
action="./users"
|
||||
class="flex flex gap-2 text-black dark:text-white text-sm">
|
||||
<input type="hidden" id="operation" name="operation" value="UPDATE" />
|
||||
<input type="hidden" id="user" name="user" value="{{ $user.ID }}" />
|
||||
<input type="hidden" id="is_admin" name="is_admin" value="false" />
|
||||
<button {{ if $user.Admin }}type="submit"{{ else }}type="button"{{ end }} class="px-2 py-1 rounded-md text-white dark:text-black {{ $userStyle }}">user
|
||||
</form>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $user.CreatedAt }}</p>
|
||||
|
||||
@@ -1,43 +1,67 @@
|
||||
{{ template "base" . }}
|
||||
{{ define "title" }}Admin - General{{ end }}
|
||||
{{ define "header" }}<a class="whitespace-pre" href="./admin">Admin - General</a>{{ end }}
|
||||
{{ define "header" }}
|
||||
<a class="whitespace-pre" href="./admin">Admin - General</a>
|
||||
{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="w-full flex flex-col gap-4 grow">
|
||||
<div class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||
<div
|
||||
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<p class="text-lg font-semibold mb-2">Backup & Restore</p>
|
||||
<div class="flex flex-col gap-4">
|
||||
<form class="flex justify-between" action="./admin" method="POST">
|
||||
<input type="text" name="action" value="BACKUP" class="hidden" />
|
||||
<div class="flex gap-8 items-center">
|
||||
<div>
|
||||
<input type="checkbox" id="backup_covers" name="backup_types" value="COVERS" />
|
||||
<input
|
||||
type="checkbox"
|
||||
id="backup_covers"
|
||||
name="backup_types"
|
||||
value="COVERS"
|
||||
/>
|
||||
<label for="backup_covers">Covers</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox"
|
||||
<input
|
||||
type="checkbox"
|
||||
id="backup_documents"
|
||||
name="backup_types"
|
||||
value="DOCUMENTS" />
|
||||
value="DOCUMENTS"
|
||||
/>
|
||||
<label for="backup_documents">Documents</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2">
|
||||
<span class="w-full">Backup</span>
|
||||
</button>
|
||||
<div class="w-40 h-10">
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Backup"
|
||||
"Variant" "Secondary"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</form>
|
||||
<form method="POST"
|
||||
<form
|
||||
method="POST"
|
||||
enctype="multipart/form-data"
|
||||
action="./admin"
|
||||
class="flex justify-between grow">
|
||||
class="flex justify-between grow"
|
||||
>
|
||||
<input type="text" name="action" value="RESTORE" class="hidden" />
|
||||
<div class="flex items-center w-1/2">
|
||||
<input type="file" accept=".zip" name="restore_file" class="w-full" />
|
||||
<input
|
||||
type="file"
|
||||
accept=".zip"
|
||||
name="restore_file"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-40 h-10">
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Restore"
|
||||
"Variant" "Secondary"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2">
|
||||
<span class="w-full">Restore</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{{ if .PasswordErrorMessage }}
|
||||
@@ -46,7 +70,9 @@
|
||||
<span class="text-green-400 text-xs">{{ .PasswordMessage }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||
<div
|
||||
class="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<p class="text-lg font-semibold">Tasks</p>
|
||||
<table class="min-w-full bg-white dark:bg-gray-700 text-sm">
|
||||
<tbody class="text-black dark:text-white">
|
||||
@@ -56,11 +82,19 @@
|
||||
</td>
|
||||
<td class="py-2 float-right">
|
||||
<form action="./admin" method="POST">
|
||||
<input type="text" name="action" value="METADATA_MATCH" class="hidden" />
|
||||
<button type="submit"
|
||||
class="w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2">
|
||||
<span class="w-full">Run</span>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
name="action"
|
||||
value="METADATA_MATCH"
|
||||
class="hidden"
|
||||
/>
|
||||
<div class="w-40 h-10 text-base">
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Run"
|
||||
"Variant" "Secondary"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -70,11 +104,19 @@
|
||||
</td>
|
||||
<td class="py-2 float-right">
|
||||
<form action="./admin" method="POST">
|
||||
<input type="text" name="action" value="CACHE_TABLES" class="hidden" />
|
||||
<button type="submit"
|
||||
class="w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2">
|
||||
<span class="w-full">Run</span>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
name="action"
|
||||
value="CACHE_TABLES"
|
||||
class="hidden"
|
||||
/>
|
||||
<div class="w-40 h-10 text-base">
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Run"
|
||||
"Variant" "Secondary"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -4,15 +4,24 @@
|
||||
{{ 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-44 md:w-60 lg:w-80 mr-4 mb-2 relative">
|
||||
<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-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="/documents/{{.Data.ID}}/cover" />
|
||||
<img
|
||||
class="rounded object-fill w-full"
|
||||
src="/documents/{{ .Data.ID }}/cover"
|
||||
/>
|
||||
</label>
|
||||
{{ if .Data.Filepath }}
|
||||
<a href="/reader#id={{ .Data.ID }}&type=REMOTE"
|
||||
class="z-10 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded text-sm text-center py-1 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Read</a>
|
||||
<a
|
||||
href="/reader#id={{ .Data.ID }}&type=REMOTE"
|
||||
class="z-10 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded text-sm text-center py-1 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>Read</a
|
||||
>
|
||||
{{ end }}
|
||||
<div class="flex flex-wrap-reverse justify-between z-20 gap-2 relative">
|
||||
<div class="min-w-[50%] md:mr-2">
|
||||
@@ -25,74 +34,114 @@
|
||||
<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-500">
|
||||
<input type="checkbox" id="edit-cover-button" class="hidden css-button" />
|
||||
<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"
|
||||
<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-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"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm">
|
||||
<input type="file" id="cover_file" name="cover_file">
|
||||
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit">Upload Cover</button>
|
||||
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm"
|
||||
>
|
||||
<input type="file" id="cover_file" name="cover_file" />
|
||||
{{ template "component/button" (dict "Title" "Upload Cover") }}
|
||||
</form>
|
||||
<form method="POST"
|
||||
<form
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm">
|
||||
<input type="checkbox"
|
||||
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 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit">Remove Cover</button>
|
||||
class="hidden"
|
||||
/>
|
||||
{{ template "component/button" (dict "Title" "Remove Cover") }}
|
||||
</form>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label for="delete-button" class="cursor-pointer">{{ template "svg/delete" (dict "Size" 28) }}</label>
|
||||
<input type="checkbox" id="delete-button" class="hidden css-button" />
|
||||
<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"
|
||||
<label for="delete-button" class="cursor-pointer"
|
||||
>{{ template "svg/delete" (dict "Size" 28) }}</label
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="delete-button"
|
||||
class="hidden css-button"
|
||||
/>
|
||||
<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 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit">Delete</button>
|
||||
class="text-black dark:text-white text-sm w-24"
|
||||
>
|
||||
{{ template "component/button" (dict "Title" "Delete") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<a href="../activity?document={{ .Data.ID }}">{{ template "svg/activity" (dict "Size" 28) }}</a>
|
||||
<a href="../activity?document={{ .Data.ID }}"
|
||||
>{{ template "svg/activity" (dict "Size" 28) }}</a
|
||||
>
|
||||
<div class="relative">
|
||||
<label for="search-button">{{ template "svg/search" (dict "Size" 28) }}</label>
|
||||
<input type="checkbox" id="search-button" class="hidden css-button" />
|
||||
<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"
|
||||
<label for="search-button"
|
||||
>{{ template "svg/search" (dict "Size" 28) }}</label
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="search-button"
|
||||
class="hidden css-button"
|
||||
/>
|
||||
<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"
|
||||
class="flex flex-col gap-2 text-black dark:text-white text-sm">
|
||||
<input type="text"
|
||||
class="flex flex-col gap-2 text-black dark:text-white text-sm"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
placeholder="Title"
|
||||
value="{{ or .Data.Title nil }}"
|
||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white">
|
||||
<input type="text"
|
||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="author"
|
||||
name="author"
|
||||
placeholder="Author"
|
||||
value="{{ or .Data.Author nil }}"
|
||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white">
|
||||
<input type="text"
|
||||
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"
|
||||
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 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit">Identify</button>
|
||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
{{ template "component/button" (dict "Title" "Identify") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .Data.Filepath }}
|
||||
<a href="./{{.Data.ID}}/file">{{ template "svg/download" (dict "Size" 28) }}</a>
|
||||
<a href="./{{ .Data.ID }}/file"
|
||||
>{{ template "svg/download" (dict "Size" 28) }}</a
|
||||
>
|
||||
{{ else }}
|
||||
{{ template "svg/download" (dict "Size" 28 "Disabled" true) }}
|
||||
{{ end }}
|
||||
@@ -100,49 +149,39 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 justify-between gap-4 pb-4">
|
||||
<div class="relative">
|
||||
<div class="text-gray-500 inline-flex gap-2 relative">
|
||||
<p>Title</p>
|
||||
<label class="my-auto" for="edit-title-button">{{ template "svg/edit" (dict "Size" 18) }}</label>
|
||||
<input type="checkbox" id="edit-title-button" class="hidden css-button" />
|
||||
<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"
|
||||
class="flex flex-col gap-2 text-black dark:text-white text-sm">
|
||||
<input type="text" id="title" name="title" value="{{ or .Data.Title "N/A" }}" 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 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-medium text-lg">{{ or .Data.Title "N/A" }}</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="text-gray-500 inline-flex gap-2 relative">
|
||||
<p>Author</p>
|
||||
<label class="my-auto" for="edit-author-button">{{ template "svg/edit" (dict "Size" 18) }}</label>
|
||||
<input type="checkbox" id="edit-author-button" class="hidden css-button" />
|
||||
<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"
|
||||
class="flex flex-col gap-2 text-black dark:text-white text-sm">
|
||||
<input type="text" id="author" name="author" value="{{ or .Data.Author "N/A" }}" 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 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-medium text-lg">{{ or .Data.Author "N/A" }}</p>
|
||||
</div>
|
||||
{{ template "component/key-val-edit" (dict
|
||||
"Title" "Title"
|
||||
"Value" .Data.Title
|
||||
"URL" (printf "./%s/edit" .Data.ID)
|
||||
"FormValue" "title"
|
||||
)
|
||||
}}
|
||||
{{ template "component/key-val-edit" (dict
|
||||
"Title" "Author"
|
||||
"Value" .Data.Author
|
||||
"URL" (printf "./%s/edit" .Data.ID)
|
||||
"FormValue" "author"
|
||||
)
|
||||
}}
|
||||
<div class="relative">
|
||||
<div class="text-gray-500 inline-flex gap-2 relative">
|
||||
<p>Time Read</p>
|
||||
<label class="my-auto" for="progress-info-button">{{ template "svg/info" (dict "Size" 18) }}</label>
|
||||
<input type="checkbox" id="progress-info-button" class="hidden css-button" />
|
||||
<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">
|
||||
<label class="my-auto" for="progress-info-button"
|
||||
>{{ template "svg/info" (dict "Size" 18) }}</label
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="progress-info-button"
|
||||
class="hidden css-button"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
<div class="text-xs flex">
|
||||
<p class="text-gray-400 w-32">Seconds / Percent</p>
|
||||
<p class="font-medium dark:text-white">{{ .Data.SecondsPerPercent }}</p>
|
||||
<p class="font-medium dark:text-white">
|
||||
{{ .Data.SecondsPerPercent }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-xs flex">
|
||||
<p class="text-gray-400 w-32">Words / Minute</p>
|
||||
@@ -150,11 +189,15 @@
|
||||
</div>
|
||||
<div class="text-xs flex">
|
||||
<p class="text-gray-400 w-32">Est. Time Left</p>
|
||||
<p class="font-medium dark:text-white whitespace-nowrap">{{ niceSeconds .TotalTimeLeftSeconds }}</p>
|
||||
<p class="font-medium dark:text-white whitespace-nowrap">
|
||||
{{ niceSeconds .TotalTimeLeftSeconds }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-medium text-lg">{{ niceSeconds .Data.TotalTimeSeconds }}</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ niceSeconds .Data.TotalTimeSeconds }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500">Progress</p>
|
||||
@@ -164,136 +207,48 @@
|
||||
<div class="relative">
|
||||
<div class="text-gray-500 inline-flex gap-2 relative">
|
||||
<p>Description</p>
|
||||
<label class="my-auto" for="edit-description-button">{{ template "svg/edit" (dict "Size" 18) }}</label>
|
||||
<label class="my-auto" for="edit-description-button"
|
||||
>{{ template "svg/edit" (dict "Size" 18) }}</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative font-medium text-justify hyphens-auto">
|
||||
<input type="checkbox"
|
||||
<input
|
||||
type="checkbox"
|
||||
id="edit-description-button"
|
||||
class="hidden css-button" />
|
||||
<div 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-44 md:w-60 lg:w-80 object-fill"
|
||||
src="/documents/{{.Data.ID}}/cover" />
|
||||
<form method="POST"
|
||||
class="hidden css-button"
|
||||
/>
|
||||
<div
|
||||
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-44 md:w-60 lg:w-80 object-fill"
|
||||
src="/documents/{{ .Data.ID }}/cover"
|
||||
/>
|
||||
<form
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
class="flex flex-col gap-2 w-full text-black bg-gray-200 rounded shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3">
|
||||
<textarea type="text"
|
||||
class="flex flex-col gap-2 w-full text-black bg-gray-200 rounded shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
|
||||
>
|
||||
<textarea
|
||||
type="text"
|
||||
id="description"
|
||||
name="description"
|
||||
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 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit">Save</button>
|
||||
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
|
||||
>
|
||||
{{ template "component/button" (dict "Title" "Save") }}
|
||||
</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="/documents/{{ .Data.ID }}"
|
||||
class="w-full text-center font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit">Back to Document</a>
|
||||
</div>
|
||||
{{ template "component/metadata" (dict
|
||||
"ID" .Data.ID
|
||||
"Metadata" .Metadata
|
||||
"Error" .MetadataError
|
||||
)
|
||||
}}
|
||||
</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="/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.ID }}?fife=w480-h690" />
|
||||
</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.ID }}">
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex justify-end gap-4 m-4">
|
||||
<a href="/documents/{{ .Data.ID }}"
|
||||
class="w-24 text-center font-medium px-2 py-1 text-white 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: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;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.css-button+div {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
{{ end }}
|
||||
|
||||
@@ -2,119 +2,98 @@
|
||||
{{ define "title" }}Documents{{ end }}
|
||||
{{ define "header" }}<a href="./documents">Documents</a>{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||
<form class="flex gap-4 flex-col lg:flex-row"
|
||||
<div
|
||||
class="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<form
|
||||
class="flex gap-4 flex-col lg:flex-row"
|
||||
action="./documents"
|
||||
method="GET">
|
||||
method="GET"
|
||||
>
|
||||
<div class="flex flex-col w-full grow">
|
||||
<div class="flex relative">
|
||||
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
{{ template "svg/search2" (dict "Size" 15) }}
|
||||
</span>
|
||||
<input type="text"
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
name="search"
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
placeholder="Search Author / Title" />
|
||||
placeholder="Search Author / Title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2">
|
||||
<span class="w-full">Search</span>
|
||||
</button>
|
||||
<div class="lg:w-60">
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Search"
|
||||
"Variant" "Secondary"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{{ range $doc := .Data }}
|
||||
<div class="w-full relative">
|
||||
<div class="flex gap-4 w-full h-full p-4 shadow-lg bg-white dark:bg-gray-700 rounded">
|
||||
<div class="min-w-fit my-auto h-48 relative">
|
||||
<a href="./documents/{{$doc.ID}}">
|
||||
<img class="rounded object-cover h-full"
|
||||
src="./documents/{{$doc.ID}}/cover" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Title</p>
|
||||
<p class="font-medium">{{ or $doc.Title "Unknown" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Author</p>
|
||||
<p class="font-medium">{{ or $doc.Author "Unknown" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Progress</p>
|
||||
<p class="font-medium">{{ $doc.Percentage }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Time Read</p>
|
||||
<p class="font-medium">{{ niceSeconds $doc.TotalTimeSeconds }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400">
|
||||
<a href="./activity?document={{ $doc.ID }}">{{ template "svg/activity" }}</a>
|
||||
{{ if $doc.Filepath }}
|
||||
<a href="./documents/{{$doc.ID}}/file">{{ template "svg/download" }}</a>
|
||||
{{ else }}
|
||||
{{ template "svg/download" (dict "Disabled" true) }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ template "component/document-card" $doc }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="w-full flex gap-4 justify-center mt-4 text-black dark:text-white">
|
||||
{{ if .PreviousPage }}
|
||||
<a href="./documents?page={{ .PreviousPage }}&limit={{ .PageLimit }}"
|
||||
class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none">◄</a>
|
||||
<a
|
||||
href="./documents?page={{ .PreviousPage }}&limit={{ .PageLimit }}"
|
||||
class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none"
|
||||
>◄</a
|
||||
>
|
||||
{{ end }}
|
||||
{{ if .NextPage }}
|
||||
<a href="./documents?page={{ .NextPage }}&limit={{ .PageLimit }}"
|
||||
class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none">►</a>
|
||||
<a
|
||||
href="./documents?page={{ .NextPage }}&limit={{ .PageLimit }}"
|
||||
class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none"
|
||||
>►</a
|
||||
>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="fixed bottom-6 right-6 rounded-full flex items-center justify-center">
|
||||
<div
|
||||
class="fixed bottom-6 right-6 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<input type="checkbox" id="upload-file-button" class="hidden css-button" />
|
||||
<div class="rounded p-4 bg-gray-800 dark:bg-gray-200 text-white dark:text-black w-72 text-sm flex flex-col gap-2">
|
||||
<form method="POST"
|
||||
<div
|
||||
class="absolute right-0 z-10 bottom-0 rounded p-4 bg-gray-800 dark:bg-gray-200 text-white dark:text-black w-72 text-sm flex flex-col gap-2"
|
||||
>
|
||||
<form
|
||||
method="POST"
|
||||
enctype="multipart/form-data"
|
||||
action="./documents"
|
||||
class="flex flex-col gap-2">
|
||||
<input type="file" accept=".epub" id="document_file" name="document_file">
|
||||
<button class="font-medium px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
type="submit">Upload File</button>
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept=".epub"
|
||||
id="document_file"
|
||||
name="document_file"
|
||||
/>
|
||||
<button
|
||||
class="font-medium px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
type="submit"
|
||||
>
|
||||
Upload File
|
||||
</button>
|
||||
</form>
|
||||
<label for="upload-file-button">
|
||||
<div class="w-full text-center cursor-pointer font-medium mt-2 px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<div
|
||||
class="w-full text-center cursor-pointer font-medium mt-2 px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
Cancel Upload
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<label class="w-16 h-16 bg-gray-800 dark:bg-gray-200 rounded-full flex items-center justify-center opacity-30 hover:opacity-100 transition-all duration-200 cursor-pointer"
|
||||
for="upload-file-button">{{ template "svg/upload" (dict "Size" 34) }}</label>
|
||||
<label
|
||||
class="w-16 h-16 bg-gray-800 dark:bg-gray-200 rounded-full flex items-center justify-center opacity-30 hover:opacity-100 transition-all duration-200 cursor-pointer"
|
||||
for="upload-file-button"
|
||||
>{{ template "svg/upload" (dict "Size" 34) }}</label
|
||||
>
|
||||
</div>
|
||||
<style>
|
||||
.css-button:checked+div {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.css-button+div {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.css-button:checked+div+label {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
{{ end }}
|
||||
|
||||
@@ -1,30 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent" />
|
||||
<meta name="theme-color"
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#F3F4F6"
|
||||
media="(prefers-color-scheme: light)" />
|
||||
<meta name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#1F2937"
|
||||
media="(prefers-color-scheme: dark)" />
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<title>AnthoLume - Error</title>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-gray-800 flex flex-col justify-center h-screen">
|
||||
<body
|
||||
class="bg-gray-100 dark:bg-gray-800 flex flex-col justify-center h-screen"
|
||||
>
|
||||
<div class="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
|
||||
<div class="mx-auto max-w-screen-sm text-center">
|
||||
<h1 class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-gray-600 dark:text-gray-500">{{ .Status }}</h1>
|
||||
<p class="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white">{{ .Error }}</p>
|
||||
<p class="mb-8 text-lg font-light text-gray-500 dark:text-gray-400">{{ .Message }}</p>
|
||||
<a href="/"
|
||||
class="rounded text-center font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100">Back to Homepage</a>
|
||||
<h1
|
||||
class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-gray-600 dark:text-gray-500"
|
||||
>
|
||||
{{ .Status }}
|
||||
</h1>
|
||||
<p
|
||||
class="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white"
|
||||
>
|
||||
{{ .Error }}
|
||||
</p>
|
||||
<p class="mb-8 text-lg font-light text-gray-500 dark:text-gray-400">
|
||||
{{ .Message }}
|
||||
</p>
|
||||
<a
|
||||
href="/"
|
||||
class="rounded text-center font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
>Back to Homepage</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -5,34 +5,52 @@
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="w-full">
|
||||
<div class="relative w-full bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<p class="absolute top-3 left-5 text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||||
<p
|
||||
class="absolute top-3 left-5 text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500"
|
||||
>
|
||||
Daily Read Totals
|
||||
</p>
|
||||
{{ $data := (getSVGGraphData .Data.GraphData 800 70 ) }}
|
||||
<div class="relative">
|
||||
<svg viewBox="26 0 755 {{ $data.Height }}"
|
||||
<svg
|
||||
viewBox="26 0 755 {{ $data.Height }}"
|
||||
preserveAspectRatio="none"
|
||||
width="100%"
|
||||
height="6em">
|
||||
height="6em"
|
||||
>
|
||||
<!-- Bezier Line Graph -->
|
||||
<path fill="#316BBE" fill-opacity="0.5" stroke="none" d="{{ $data.BezierPath }} {{ $data.BezierFill }}" />
|
||||
<path
|
||||
fill="#316BBE"
|
||||
fill-opacity="0.5"
|
||||
stroke="none"
|
||||
d="{{ $data.BezierPath }} {{ $data.BezierFill }}"
|
||||
/>
|
||||
<path fill="none" stroke="#316BBE" d="{{ $data.BezierPath }}" />
|
||||
</svg>
|
||||
<div class="flex absolute w-full h-full top-0"
|
||||
<div
|
||||
class="flex absolute w-full h-full top-0"
|
||||
style="width: calc(100%*31/30);
|
||||
transform: translateX(-50%);
|
||||
left: 50%">
|
||||
left: 50%"
|
||||
>
|
||||
{{ range $index, $item := $data.LinePoints }}
|
||||
<!-- Required for iOS "Hover" Events (onclick) -->
|
||||
<div onclick
|
||||
<div
|
||||
onclick
|
||||
class="opacity-0 hover:opacity-100 w-full"
|
||||
style="background: linear-gradient(rgba(128, 128, 128, 0.5), rgba(128, 128, 128, 0.5)) no-repeat center/2px 100%">
|
||||
<div class="flex flex-col items-center p-2 rounded absolute top-3 dark:text-white text-xs pointer-events-none"
|
||||
style="background: linear-gradient(rgba(128, 128, 128, 0.5), rgba(128, 128, 128, 0.5)) no-repeat center/2px 100%"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-center p-2 rounded absolute top-3 dark:text-white text-xs pointer-events-none"
|
||||
style="transform: translateX(-50%);
|
||||
background-color: rgba(128, 128, 128, 0.2);
|
||||
left: 50%">
|
||||
left: 50%"
|
||||
>
|
||||
<span>{{ (index $.Data.GraphData $index).Date }}</span>
|
||||
<span>{{ (index $.Data.GraphData $index).MinutesRead }} minutes</span>
|
||||
<span
|
||||
>{{ (index $.Data.GraphData $index).MinutesRead }}
|
||||
minutes</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -41,97 +59,51 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
<a href="./documents" class="w-full">
|
||||
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<p class="text-2xl font-bold text-black dark:text-white">{{ .Data.DatabaseInfo.DocumentsSize }}</p>
|
||||
<p class="text-sm text-gray-400">Documents</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="./activity" class="w-full">
|
||||
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<p class="text-2xl font-bold text-black dark:text-white">{{ .Data.DatabaseInfo.ActivitySize }}</p>
|
||||
<p class="text-sm text-gray-400">Activity Records</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="./progress" class="w-full">
|
||||
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<p class="text-2xl font-bold text-black dark:text-white">{{ .Data.DatabaseInfo.ProgressSize }}</p>
|
||||
<p class="text-sm text-gray-400">Progress Records</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="w-full">
|
||||
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<p class="text-2xl font-bold text-black dark:text-white">{{ .Data.DatabaseInfo.DevicesSize }}</p>
|
||||
<p class="text-sm text-gray-400">Devices</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ template "component/info-card" (dict
|
||||
"Title" "Documents"
|
||||
"Size" .Data.DatabaseInfo.DocumentsSize
|
||||
"Link" "./documents"
|
||||
)
|
||||
}}
|
||||
{{ template "component/info-card" (dict
|
||||
"Title" "Activity Records"
|
||||
"Size" .Data.DatabaseInfo.ActivitySize
|
||||
"Link" "./activity"
|
||||
)
|
||||
}}
|
||||
{{ template "component/info-card" (dict
|
||||
"Title" "Progress Records"
|
||||
"Size" .Data.DatabaseInfo.ProgressSize
|
||||
"Link" "./progress"
|
||||
)
|
||||
}}
|
||||
{{ template "component/info-card" (dict
|
||||
"Title" "Devices"
|
||||
"Size" .Data.DatabaseInfo.DevicesSize
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{{ range $item := .Data.Streaks }}
|
||||
<div class="w-full">
|
||||
<div class="relative w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<p class="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||||
{{ if eq $item.Window "WEEK" }}
|
||||
Weekly Read Streak
|
||||
{{ else }}
|
||||
Daily Read Streak
|
||||
{{ end }}
|
||||
</p>
|
||||
<div class="flex items-end my-6 space-x-2">
|
||||
<p class="text-5xl font-bold text-black dark:text-white">{{ $item.CurrentStreak }}</p>
|
||||
</div>
|
||||
<div class="dark:text-white">
|
||||
<div class="flex items-center justify-between pb-2 mb-2 text-sm border-b border-gray-200">
|
||||
<div>
|
||||
<p>
|
||||
{{ if eq $item.Window "WEEK" }} Current Weekly Streak {{ else }}
|
||||
Current Daily Streak {{ end }}
|
||||
</p>
|
||||
<div class="flex items-end text-sm text-gray-400">
|
||||
{{ $item.CurrentStreakStartDate }} ➞ {{ $item.CurrentStreakEndDate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{{ $item.CurrentStreak }}</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between pb-2 mb-2 text-sm">
|
||||
<div>
|
||||
<p>
|
||||
{{ if eq $item.Window "WEEK" }}
|
||||
Best Weekly Streak
|
||||
{{ else }}
|
||||
Best Daily Streak
|
||||
{{ end }}
|
||||
</p>
|
||||
<div class="flex items-end text-sm text-gray-400">{{ $item.MaxStreakStartDate }} ➞ {{ $item.MaxStreakEndDate }}</div>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{{ $item.MaxStreak }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ template "component/streak-card" $item }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{{ template "component/leaderboard-card" (dict
|
||||
"Name" "WPM"
|
||||
"Data" .Data.UserStatistics.WPM
|
||||
)}}
|
||||
)
|
||||
}}
|
||||
{{ template "component/leaderboard-card" (dict
|
||||
"Name" "Duration"
|
||||
"Data" .Data.UserStatistics.Duration
|
||||
)}}
|
||||
)
|
||||
}}
|
||||
{{ template "component/leaderboard-card" (dict
|
||||
"Name" "Words"
|
||||
"Data" .Data.UserStatistics.Words
|
||||
)}}
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent" />
|
||||
<meta name="theme-color"
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#F3F4F6"
|
||||
media="(prefers-color-scheme: light)" />
|
||||
<meta name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#1F2937"
|
||||
media="(prefers-color-scheme: dark)" />
|
||||
<title>AnthoLume - {{ if .Register }}Register{{ else }}Login{{ end }}</title>
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<title>
|
||||
AnthoLume - {{ if .Register }}Register{{ else }}Login{{ end }}
|
||||
</title>
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<link rel="stylesheet" href="./assets/style.css" />
|
||||
<!-- Service Worker / Offline Cache Flush -->
|
||||
@@ -32,7 +42,8 @@
|
||||
|
||||
html {
|
||||
height: calc(100% + env(safe-area-inset-bottom));
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0
|
||||
env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
/* No Scrollbar - IE, Edge, Firefox */
|
||||
@@ -50,43 +61,60 @@
|
||||
<body class="bg-gray-100 dark:bg-gray-800 dark:text-white">
|
||||
<div class="flex flex-wrap w-full">
|
||||
<div class="flex flex-col w-full md:w-1/2">
|
||||
<div class="flex flex-col justify-center px-8 pt-8 my-auto md:justify-start md:pt-0 md:px-24 lg:px-32">
|
||||
<div
|
||||
class="flex flex-col justify-center px-8 pt-8 my-auto md:justify-start md:pt-0 md:px-24 lg:px-32"
|
||||
>
|
||||
<p class="text-3xl text-center">Welcome.</p>
|
||||
<form
|
||||
class="flex flex-col pt-3 md:pt-8"
|
||||
{{ if
|
||||
.Register}}action="./register"
|
||||
{{ else }}action="./login"
|
||||
.Register
|
||||
}}
|
||||
action="./register"
|
||||
{{ else }}
|
||||
action="./login"
|
||||
{{ end }}
|
||||
method="POST"
|
||||
>
|
||||
<div class="flex flex-col pt-4">
|
||||
<div class="flex relative">
|
||||
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
{{ template "svg/user" (dict "Size" 15) }}
|
||||
</span>
|
||||
<input type="text"
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
placeholder="Username" />
|
||||
placeholder="Username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col pt-4 mb-12">
|
||||
<div class="flex relative">
|
||||
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
{{ template "svg/password" (dict "Size" 15) }}
|
||||
</span>
|
||||
<input type="password"
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
placeholder="Password" />
|
||||
<span class="absolute -bottom-5 text-red-400 text-xs">{{ .Error }}</span>
|
||||
placeholder="Password"
|
||||
/>
|
||||
<span class="absolute -bottom-5 text-red-400 text-xs"
|
||||
>{{ .Error }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full px-4 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full px-4 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
|
||||
>
|
||||
{{ if .Register }}
|
||||
<span class="w-full">Register</span>
|
||||
{{ else }}
|
||||
@@ -95,32 +123,50 @@
|
||||
</button>
|
||||
</form>
|
||||
<div class="pt-12 pb-12 text-center">
|
||||
{{ if .Config.RegistrationEnabled }} {{ if .Register }}
|
||||
{{ if .Config.RegistrationEnabled }}
|
||||
{{ if .Register }}
|
||||
<p>
|
||||
Trying to login?
|
||||
<a href="./login" class="font-semibold underline">Login here.</a>
|
||||
<a href="./login" class="font-semibold underline"
|
||||
>Login here.</a
|
||||
>
|
||||
</p>
|
||||
{{ else }}
|
||||
<p>
|
||||
Don't have an account?
|
||||
<a href="./register" class="font-semibold underline">Register here.</a>
|
||||
<a href="./register" class="font-semibold underline"
|
||||
>Register here.</a
|
||||
>
|
||||
</p>
|
||||
{{ end }} {{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<p class="mt-4">
|
||||
<a href="./local" class="font-semibold underline">Offline / Local Mode</a>
|
||||
<a href="./local" class="font-semibold underline"
|
||||
>Offline / Local Mode</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden image-fader w-1/2 shadow-2xl h-screen relative md:block">
|
||||
<img class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||
src="/assets/images/book1.jpg" />
|
||||
<img class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||
src="/assets/images/book2.jpg" />
|
||||
<img class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||
src="/assets/images/book3.jpg" />
|
||||
<img class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||
src="/assets/images/book4.jpg" />
|
||||
<div
|
||||
class="hidden image-fader w-1/2 shadow-2xl h-screen relative md:block"
|
||||
>
|
||||
<img
|
||||
class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||
src="/assets/images/book1.jpg"
|
||||
/>
|
||||
<img
|
||||
class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||
src="/assets/images/book2.jpg"
|
||||
/>
|
||||
<img
|
||||
class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||
src="/assets/images/book3.jpg"
|
||||
/>
|
||||
<img
|
||||
class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||
src="/assets/images/book4.jpg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
|
||||
@@ -4,41 +4,25 @@
|
||||
{{ define "content" }}
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||
<thead class="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Document</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Device</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Percent</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-black dark:text-white">
|
||||
{{ if not .Data }}
|
||||
<tr>
|
||||
<td class="text-center p-3" colspan="4">No Results</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range $progress := .Data }}
|
||||
<tr>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<a href="./documents/{{ $progress.DocumentID }}">{{ $progress.Author }} - {{ $progress.Title }}
|
||||
</p>
|
||||
</a>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $progress.DeviceName }}</p>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $progress.Percentage }}%</p>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $progress.CreatedAt }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Table Component - Utilizes Template "table-cell" -->
|
||||
{{ template "component/table" (dict
|
||||
"Columns" (slice "Document" "Device Name" "Percentage" "Created At")
|
||||
"Keys" (slice "Document" "DeviceName" "Percentage" "CreatedAt")
|
||||
"Rows" .Data
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<!-- Table Cell Definition -->
|
||||
{{ define "table-cell" }}
|
||||
{{ if eq .Name "Document" }}
|
||||
<a href="./documents/{{ .Data.DocumentID }}"
|
||||
>{{ .Data.Author }} - {{ .Data.Title }}</a
|
||||
>
|
||||
{{ else if eq .Name "Percentage" }}
|
||||
{{ index (fields .Data) .Name }}%
|
||||
{{ else }}
|
||||
{{ index (fields .Data) .Name }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -4,57 +4,92 @@
|
||||
{{ define "content" }}
|
||||
<div class="w-full flex flex-col md:flex-row gap-4">
|
||||
<div class="flex flex-col gap-4 grow">
|
||||
<div class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||
<div
|
||||
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<form class="flex gap-4 flex-col lg:flex-row" action="./search">
|
||||
<div class="flex flex-col w-full grow">
|
||||
<div class="flex relative">
|
||||
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
{{ template "svg/search2" (dict "Size" 15) }}
|
||||
</span>
|
||||
<input type="text"
|
||||
<input
|
||||
type="text"
|
||||
id="query"
|
||||
name="query"
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
placeholder="Query" />
|
||||
placeholder="Query"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex relative min-w-[12em]">
|
||||
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
{{ template "svg/documents" (dict "Size" 15) }}
|
||||
</span>
|
||||
<select class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
<select
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
id="source"
|
||||
name="source">
|
||||
name="source"
|
||||
>
|
||||
<option value="Annas Archive">Annas Archive</option>
|
||||
<option value="LibGen Fiction">LibGen Fiction</option>
|
||||
<option value="LibGen Non-fiction">LibGen Non-fiction</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2">
|
||||
<span class="w-full">Search</span>
|
||||
</button>
|
||||
<div class="lg:w-60">
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Search"
|
||||
"Variant" "Secondary"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</form>
|
||||
{{ if .SearchErrorMessage }}
|
||||
<span class="text-red-400 text-xs">{{ .SearchErrorMessage }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm md:text-sm">
|
||||
<table
|
||||
class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm md:text-sm"
|
||||
>
|
||||
<thead class="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col"
|
||||
class="w-12 p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"></th>
|
||||
<th scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Document</th>
|
||||
<th scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Series</th>
|
||||
<th scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Type</th>
|
||||
<th scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Size</th>
|
||||
<th scope="col"
|
||||
class="p-3 hidden md:block font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||
<th
|
||||
scope="col"
|
||||
class="w-12 p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
></th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Document
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Series
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Size
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-3 hidden md:block font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Date
|
||||
</th>
|
||||
</tr>
|
||||
@@ -64,29 +99,42 @@
|
||||
<tr>
|
||||
<td class="text-center p-3" colspan="6">No Results</td>
|
||||
</tr>
|
||||
{{ end }} {{ range $item := .Data }}
|
||||
{{ end }}
|
||||
{{ range $item := .Data }}
|
||||
<tr>
|
||||
<td class="p-3 border-b border-gray-200 text-gray-500 dark:text-gray-500">
|
||||
<td
|
||||
class="p-3 border-b border-gray-200 text-gray-500 dark:text-gray-500"
|
||||
>
|
||||
<form action="./search" method="POST">
|
||||
<input class="hidden"
|
||||
<input
|
||||
class="hidden"
|
||||
type="text"
|
||||
id="source"
|
||||
name="source"
|
||||
value="{{ $.Source }}" />
|
||||
<input class="hidden"
|
||||
value="{{ $.Source }}"
|
||||
/>
|
||||
<input
|
||||
class="hidden"
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value="{{ $item.Title }}" />
|
||||
<input class="hidden"
|
||||
value="{{ $item.Title }}"
|
||||
/>
|
||||
<input
|
||||
class="hidden"
|
||||
type="text"
|
||||
id="author"
|
||||
name="author"
|
||||
value="{{ $item.Author }}" />
|
||||
<button name="id" value="{{ $item.ID }}">{{ template "svg/download" }}</button>
|
||||
value="{{ $item.Author }}"
|
||||
/>
|
||||
<button name="id" value="{{ $item.ID }}">
|
||||
{{ template "svg/download" }}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">{{ $item.Author }} - {{ $item.Title }}</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
{{ $item.Author }} - {{ $item.Title }}
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ or $item.Series "N/A" }}</p>
|
||||
</td>
|
||||
|
||||
@@ -4,45 +4,62 @@
|
||||
{{ define "content" }}
|
||||
<div class="w-full flex flex-col md:flex-row gap-4">
|
||||
<div>
|
||||
<div class="flex flex-col p-4 items-center rounded shadow-lg md:w-60 lg:w-80 bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||
<div
|
||||
class="flex flex-col p-4 items-center rounded shadow-lg md:w-60 lg:w-80 bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
{{ template "svg/user" (dict "Size" 60) }}
|
||||
<p class="text-lg">{{ .Authorization.UserName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 grow">
|
||||
<div class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||
<div
|
||||
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<p class="text-lg font-semibold mb-2">Change Password</p>
|
||||
<form class="flex gap-4 flex-col lg:flex-row"
|
||||
<form
|
||||
class="flex gap-4 flex-col lg:flex-row"
|
||||
action="./settings"
|
||||
method="POST">
|
||||
method="POST"
|
||||
>
|
||||
<div class="flex flex-col grow">
|
||||
<div class="flex relative">
|
||||
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
{{ template "svg/password" (dict "Size" 15) }}
|
||||
</span>
|
||||
<input type="password"
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
placeholder="Password" />
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col grow">
|
||||
<div class="flex relative">
|
||||
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
{{ template "svg/password" (dict "Size" 15) }}
|
||||
</span>
|
||||
<input type="password"
|
||||
<input
|
||||
type="password"
|
||||
id="new_password"
|
||||
name="new_password"
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
placeholder="New Password" />
|
||||
placeholder="New Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2">
|
||||
<span class="w-full">Submit</span>
|
||||
</button>
|
||||
<div class="lg:w-60">
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Submit"
|
||||
"Variant" "Secondary"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</form>
|
||||
{{ if .PasswordErrorMessage }}
|
||||
<span class="text-red-400 text-xs">{{ .PasswordErrorMessage }}</span>
|
||||
@@ -50,51 +67,77 @@
|
||||
<span class="text-green-400 text-xs">{{ .PasswordMessage }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||
<p class="text-lg font-semibold mb-2">Change Time Offset</p>
|
||||
<form class="flex gap-4 flex-col lg:flex-row"
|
||||
<div
|
||||
class="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<p class="text-lg font-semibold mb-2">Change Timezone</p>
|
||||
<form
|
||||
class="flex gap-4 flex-col lg:flex-row"
|
||||
action="./settings"
|
||||
method="POST">
|
||||
method="POST"
|
||||
>
|
||||
<div class="flex relative grow">
|
||||
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
{{ template "svg/clock" (dict "Size" 15) }}
|
||||
</span>
|
||||
<select class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
id="time_offset"
|
||||
name="time_offset">
|
||||
{{ range $item := getUTCOffsets }}
|
||||
<option {{ if (eq $item.Value $.Data.TimeOffset) }}selected{{ end }} value="{{ $item.Value }}">
|
||||
{{ $item.Name }}
|
||||
<select
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
id="timezone"
|
||||
name="timezone"
|
||||
>
|
||||
{{ range $item := getTimeZones }}
|
||||
<option
|
||||
{{ if (eq $item $.Data.Timezone) }}selected{{ end }}
|
||||
value="{{ $item }}"
|
||||
>
|
||||
{{ $item }}
|
||||
</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2">
|
||||
<span class="w-full">Submit</span>
|
||||
</button>
|
||||
<div class="lg:w-60">
|
||||
{{ template "component/button" (dict
|
||||
"Title" "Submit"
|
||||
"Variant" "Secondary"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</form>
|
||||
{{ if .TimeOffsetErrorMessage }}
|
||||
<span class="text-red-400 text-xs">{{ .TimeOffsetErrorMessage }}</span>
|
||||
<span class="text-red-400 text-xs"
|
||||
>{{ .TimeOffsetErrorMessage }}</span
|
||||
>
|
||||
{{ else if .TimeOffsetMessage }}
|
||||
<span class="text-green-400 text-xs">{{ .TimeOffsetMessage }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||
<div
|
||||
class="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<p class="text-lg font-semibold">Devices</p>
|
||||
<table class="min-w-full bg-white dark:bg-gray-700 text-sm">
|
||||
<thead class="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col"
|
||||
class="p-3 pl-0 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||
<th
|
||||
scope="col"
|
||||
class="p-3 pl-0 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||
<th
|
||||
scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Last Sync
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Created</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Created
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-black dark:text-white">
|
||||
|
||||
Reference in New Issue
Block a user