Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2976dad4e5 | ||
|
|
acc9d08c60 | ||
|
|
e09c55cecc | ||
|
|
6f1c13998c | ||
|
|
0a0caa9533 | ||
|
|
4716790564 | ||
|
|
8e2c1a4b3a | ||
|
|
4a870a7f18 | ||
|
|
40dabfd788 | ||
|
|
ef3c05714a | ||
|
|
816ee0ec80 | ||
|
|
73a2852ee3 | ||
|
|
f67232a20b | ||
|
|
7b465ac97e | ||
|
|
033a012069 | ||
|
|
9165c4eb3f | ||
|
|
9ef6c5b6ed | ||
|
|
987ece38f0 | ||
|
|
c5c2270007 | ||
|
|
b23f6b0bab | ||
|
|
8ea70ea7f8 | ||
|
|
07578e0206 | ||
|
|
f402a3ee03 | ||
|
|
f3f908eb7e | ||
|
|
94a4facc6c | ||
|
|
11617c1860 |
+1
-1
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
bru run --env Bookhoard --delay 500 "NewDevDBSetup/RegisterUser.yml" "NewDevDBSetup/CreateEbookLibrary.yml" "NewDevDBSetup/CreateComicLibrary.yml" "NewDevDBSetup/CreateMangaLibrary.yml" "NewDevDBSetup/AddEbookLibraryFolder.yml" "NewDevDBSetup/AddComicLibraryFolder.yml" "NewDevDBSetup/AddMangaLibraryFolder.yml" "NewDevDBSetup/ScanAllLibraries.yml"
|
||||
bru run --env Bookhoard --delay 500 "NewDevDBSetup/RegisterUser.yml" "NewDevDBSetup/SetBaseUrl.yml" "NewDevDBSetup/CreateEbookLibrary.yml" "NewDevDBSetup/CreateComicLibrary.yml" "NewDevDBSetup/CreateMangaLibrary.yml" "NewDevDBSetup/AddEbookLibraryFolder.yml" "NewDevDBSetup/AddComicLibraryFolder.yml" "NewDevDBSetup/AddMangaLibraryFolder.yml" "NewDevDBSetup/ScanAllLibraries.yml"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
info:
|
||||
name: SetBaseUrl
|
||||
type: http
|
||||
seq: 3
|
||||
|
||||
http:
|
||||
method: PUT
|
||||
url: '{{base_url}}/api/system/config'
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: |-
|
||||
{
|
||||
"base_url": "http://localhost:8765"
|
||||
}
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: Bearer {{token}}
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
|
||||
docs: |-
|
||||
## Set Base URL
|
||||
|
||||
Configures the server's base_url during initial dev database setup.
|
||||
|
||||
Must be run after RegisterUser (which provides the auth token) and before
|
||||
any library/device creation (which require setup to be complete).
|
||||
|
||||
**Method:** PUT
|
||||
**Endpoint:** /api/system/config
|
||||
**Auth:** Bearer token (from RegisterUser)
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/labstack/echo/v5"
|
||||
@@ -48,6 +50,35 @@ func main() {
|
||||
}
|
||||
log.Println("✅ Database schema initialized and verified, starting server...")
|
||||
|
||||
// Seed base_url from env var if not already configured. Uses conditional
|
||||
// UPDATE so admin-set values are never overwritten on restart.
|
||||
if cfg.BaseURL != "" {
|
||||
_, err = dbPool.Exec(ctx, `
|
||||
INSERT INTO system_config (key, value)
|
||||
VALUES ('base_url', $1)
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = EXCLUDED.value
|
||||
WHERE system_config.value = ''
|
||||
`, cfg.BaseURL)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Could not seed base_url: %v", err)
|
||||
} else {
|
||||
// Also seed derived URLs
|
||||
for key, suffix := range map[string]string{
|
||||
"opds_base_url": "/opds",
|
||||
"api_base_url": "/api",
|
||||
} {
|
||||
_, _ = dbPool.Exec(ctx, `
|
||||
INSERT INTO system_config (key, value)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = EXCLUDED.value
|
||||
WHERE system_config.value = ''
|
||||
`, key, cfg.BaseURL+suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create login attempt tracker: 5 failed attempts = 15 minute lockout
|
||||
loginAttemptTracker := ratelimit.NewLoginAttemptTracker(5, 15*time.Minute, 5*time.Minute)
|
||||
|
||||
|
||||
@@ -1149,12 +1149,14 @@ CREATE TABLE IF NOT EXISTS system_config (
|
||||
updated_by UUID REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Pre-seeded values
|
||||
INSERT INTO system_config (key, value) VALUES
|
||||
('base_url', 'https://bookhoard.example.com'),
|
||||
('opds_base_url', 'https://bookhoard.example.com/opds'),
|
||||
('api_base_url', 'https://bookhoard.example.com/api')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
-- One-time cleanup: clear the old placeholder seed so the startup logic
|
||||
-- can re-seed from the BASE_URL env var (or the setup wizard can set it).
|
||||
UPDATE system_config SET value = ''
|
||||
WHERE key = 'base_url' AND value = 'https://bookhoard.example.com';
|
||||
UPDATE system_config SET value = ''
|
||||
WHERE key = 'opds_base_url' AND value = 'https://bookhoard.example.com/opds';
|
||||
UPDATE system_config SET value = ''
|
||||
WHERE key = 'api_base_url' AND value = 'https://bookhoard.example.com/api';
|
||||
|
||||
-- Create opds_tokens table (device-specific OPDS access tokens)
|
||||
CREATE TABLE IF NOT EXISTS opds_tokens (
|
||||
|
||||
@@ -29,6 +29,7 @@ require (
|
||||
github.com/yuin/goldmark v1.8.2
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/text v0.36.0
|
||||
)
|
||||
|
||||
@@ -57,7 +58,6 @@ require (
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/xyproto/randomstring v1.2.0 // indirect
|
||||
golang.org/x/image v0.39.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
|
||||
@@ -45,30 +45,19 @@ func (c *Config) DatabaseURL() string {
|
||||
c.DatabaseUser, c.DatabasePassword, c.DatabaseHost, c.DatabasePort, c.DatabaseName)
|
||||
}
|
||||
|
||||
// GetBaseURL returns the base URL from system configuration database with fallback to config/env var
|
||||
func GetBaseURL(ctx context.Context, db interface{}) string {
|
||||
// Try to get from database first
|
||||
type SystemConfigQuerier interface {
|
||||
GetSystemConfig(ctx context.Context, key string) (SystemConfigRow, error)
|
||||
}
|
||||
// SystemConfigGetter returns the value for a system config key, or an error.
|
||||
type SystemConfigGetter func(ctx context.Context, key string) (string, error)
|
||||
|
||||
if querier, ok := db.(SystemConfigQuerier); ok {
|
||||
config, err := querier.GetSystemConfig(ctx, "base_url")
|
||||
if err == nil && config.Value != "" {
|
||||
return config.Value
|
||||
}
|
||||
// GetBaseURL returns the base URL from system configuration database, or empty
|
||||
// string if not set. The getter abstraction avoids importing the database package.
|
||||
func GetBaseURL(ctx context.Context, getter SystemConfigGetter) string {
|
||||
val, err := getter(ctx, "base_url")
|
||||
if err == nil && val != "" {
|
||||
return val
|
||||
}
|
||||
|
||||
// Fallback: return empty string - caller should use their own fallback
|
||||
return ""
|
||||
}
|
||||
|
||||
// SystemConfigRow represents a system configuration row
|
||||
type SystemConfigRow struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
|
||||
@@ -73,6 +73,7 @@ type BookInfo struct {
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
CoverImagePath string `json:"cover_image_path"`
|
||||
HasConflict bool `json:"has_conflict"`
|
||||
}
|
||||
|
||||
type SectionData struct {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/services"
|
||||
"bookhoard/internal/utils"
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -64,6 +65,7 @@ func (h *DashboardHandler) GetSections(c *echo.Context) error {
|
||||
}
|
||||
|
||||
sectionData := BuildSections(sections, libraryID)
|
||||
sectionData = MarkActiveConflictsSections(c.Request().Context(), h.db, user.ID, sectionData)
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"sections": sectionData})
|
||||
}
|
||||
@@ -188,6 +190,59 @@ func BuildSections(sections []services.DashboardSection, currentLibraryID string
|
||||
return result
|
||||
}
|
||||
|
||||
// activeConflictSet returns the set of media item IDs (as strings) that have an
|
||||
// active (unresolved) progress sync conflict for the given user. A single query
|
||||
// is issued; resolved conflicts are filtered out in memory.
|
||||
func activeConflictSet(ctx context.Context, db *database.Queries, userID pgtype.UUID) map[string]bool {
|
||||
conflicts, err := db.ListSyncConflictsByUser(ctx, userID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
set := make(map[string]bool, len(conflicts))
|
||||
for _, c := range conflicts {
|
||||
if c.ResolutionStatus.String == "unresolved" {
|
||||
set[uuid.UUID(c.MediaItemID.Bytes).String()] = true
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// MarkActiveConflicts stamps HasConflict on each book whose media item has an
|
||||
// active progress sync conflict for the user. It performs a single query
|
||||
// regardless of how many books are passed.
|
||||
func MarkActiveConflicts(ctx context.Context, db *database.Queries, userID pgtype.UUID, books []BookInfo) []BookInfo {
|
||||
if len(books) == 0 {
|
||||
return books
|
||||
}
|
||||
set := activeConflictSet(ctx, db, userID)
|
||||
for i := range books {
|
||||
if set[books[i].MediaItemID] {
|
||||
books[i].HasConflict = true
|
||||
}
|
||||
}
|
||||
return books
|
||||
}
|
||||
|
||||
// MarkActiveConflictsSections is the section-aware variant of MarkActiveConflicts,
|
||||
// used by the dashboard which renders books grouped into sections.
|
||||
func MarkActiveConflictsSections(ctx context.Context, db *database.Queries, userID pgtype.UUID, sections []SectionData) []SectionData {
|
||||
if len(sections) == 0 {
|
||||
return sections
|
||||
}
|
||||
set := activeConflictSet(ctx, db, userID)
|
||||
if len(set) == 0 {
|
||||
return sections
|
||||
}
|
||||
for s := range sections {
|
||||
for i := range sections[s].Items {
|
||||
if set[sections[s].Items[i].MediaItemID] {
|
||||
sections[s].Items[i].HasConflict = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
func getViewAllURL(collectionID string, libraryID string) string {
|
||||
if collectionID != "" {
|
||||
if libraryID != "" {
|
||||
|
||||
@@ -38,15 +38,15 @@ func NewOPDSHandler(db *database.Queries, libraryService *services.LibraryServic
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get base URL from system config
|
||||
// Helper function to get base URL from system config with request-derived fallback
|
||||
func (h *OPDSHandler) getBaseURLs(c *echo.Context) (string, string, error) {
|
||||
baseURL, err := h.db.GetSystemConfig(c.Request().Context(), "base_url")
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get base_url from config: %w", err)
|
||||
var dbBaseURL string
|
||||
if config, err := h.db.GetSystemConfig(c.Request().Context(), "base_url"); err == nil {
|
||||
dbBaseURL = config.Value
|
||||
}
|
||||
|
||||
opdsBaseURL := baseURL.Value + "/opds"
|
||||
return baseURL.Value, opdsBaseURL, nil
|
||||
baseURL := deriveBaseURL(c, dbBaseURL)
|
||||
opdsBaseURL := baseURL + "/opds"
|
||||
return baseURL, opdsBaseURL, nil
|
||||
}
|
||||
|
||||
func (h *OPDSHandler) getAuthToken(c *echo.Context) string {
|
||||
|
||||
@@ -374,6 +374,11 @@ func (h *Handler) GetAllProgressData(c *echo.Context) ([]ProgressWithMedia, erro
|
||||
deviceName = progress.LastSyncDevice.String
|
||||
}
|
||||
|
||||
lastUpdated := ""
|
||||
if progress.LastReadAt.Valid {
|
||||
lastUpdated = progress.LastReadAt.Time.Format("01-02-2006 03:04 PM")
|
||||
}
|
||||
|
||||
progressList = append(progressList, ProgressWithMedia{
|
||||
MediaItemID: progress.MediaItemID.Bytes,
|
||||
Title: mediaItem.Title,
|
||||
@@ -386,6 +391,11 @@ func (h *Handler) GetAllProgressData(c *echo.Context) ([]ProgressWithMedia, erro
|
||||
Epubcfi: epubcfi,
|
||||
LastSyncDevice: deviceName,
|
||||
ProgressPercentage: progress.Percentage.Float64 * 100,
|
||||
EpubCFI: epubcfi,
|
||||
LastUpdated: lastUpdated,
|
||||
DeviceIcon: getDeviceIcon(deviceName),
|
||||
DeviceName: deviceName,
|
||||
DeviceType: deviceName,
|
||||
FormatGroup: mediaItem.FormatGroup,
|
||||
EstimatedPages: wsync.EstimatedPages(mediaItem.TotalCharacters.Int64),
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"bookhoard/internal/config"
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/setupstatus"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -81,15 +82,13 @@ func (h *SidecarHandler) GetSidecarConfig(c *echo.Context) error {
|
||||
userID := device.UserID.Bytes
|
||||
pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
|
||||
|
||||
// Get base URL and compute paths
|
||||
baseURL, _ := h.db.GetSystemConfig(ctx, "base_url")
|
||||
if baseURL.Value == "" {
|
||||
baseURL.Value = h.cfg.BaseURL
|
||||
}
|
||||
// Get base URL and compute paths (with request-derived fallback)
|
||||
dbBaseURL, _ := h.db.GetSystemConfig(ctx, "base_url")
|
||||
baseURL := deriveBaseURL(c, dbBaseURL.Value)
|
||||
|
||||
// Generate URLs
|
||||
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", baseURL.Value, deviceID.String())
|
||||
syncAPIURL := fmt.Sprintf("%s/api/sync/kobo", baseURL.Value)
|
||||
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", baseURL, deviceID.String())
|
||||
syncAPIURL := fmt.Sprintf("%s/api/sync/kobo", baseURL)
|
||||
|
||||
// Get user's visible libraries with media items
|
||||
mediaItems, err := h.db.GetUserMediaItemsForSync(ctx, pgUserID)
|
||||
@@ -176,8 +175,8 @@ func (h *SidecarHandler) GetSidecarConfig(c *echo.Context) error {
|
||||
Bookhoard: SidecarBookhoardConfig{
|
||||
OPDSCatalog: opdsCatalogURL,
|
||||
SyncAPI: syncAPIURL,
|
||||
OPDSBaseURL: baseURL.Value + "/opds",
|
||||
APIBaseURL: baseURL.Value + "/api",
|
||||
OPDSBaseURL: baseURL + "/opds",
|
||||
APIBaseURL: baseURL + "/api",
|
||||
DeviceID: deviceID.String(),
|
||||
DeviceToken: device.AuthToken,
|
||||
},
|
||||
@@ -219,15 +218,13 @@ func (h *SidecarHandler) DownloadSidecarConfig(c *echo.Context) error {
|
||||
userID := device.UserID.Bytes
|
||||
pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
|
||||
|
||||
// Get base URL and compute paths
|
||||
baseURL, _ := h.db.GetSystemConfig(ctx, "base_url")
|
||||
if baseURL.Value == "" {
|
||||
baseURL.Value = h.cfg.BaseURL
|
||||
}
|
||||
// Get base URL and compute paths (with request-derived fallback)
|
||||
dbBaseURL, _ := h.db.GetSystemConfig(ctx, "base_url")
|
||||
baseURL := deriveBaseURL(c, dbBaseURL.Value)
|
||||
|
||||
// Generate URLs
|
||||
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", baseURL.Value, deviceID.String())
|
||||
syncAPIURL := fmt.Sprintf("%s/api/sync/kobo", baseURL.Value)
|
||||
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", baseURL, deviceID.String())
|
||||
syncAPIURL := fmt.Sprintf("%s/api/sync/kobo", baseURL)
|
||||
|
||||
// Get user's visible libraries with media items
|
||||
mediaItems, err := h.db.GetUserMediaItemsForSync(ctx, pgUserID)
|
||||
@@ -309,8 +306,8 @@ func (h *SidecarHandler) DownloadSidecarConfig(c *echo.Context) error {
|
||||
Bookhoard: SidecarBookhoardConfig{
|
||||
OPDSCatalog: opdsCatalogURL,
|
||||
SyncAPI: syncAPIURL,
|
||||
OPDSBaseURL: baseURL.Value + "/opds",
|
||||
APIBaseURL: baseURL.Value + "/api",
|
||||
OPDSBaseURL: baseURL + "/opds",
|
||||
APIBaseURL: baseURL + "/api",
|
||||
DeviceID: deviceID.String(),
|
||||
DeviceToken: device.AuthToken,
|
||||
},
|
||||
@@ -434,6 +431,10 @@ func (h *SidecarHandler) UpdateSystemConfiguration(c *echo.Context) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate setup status cache so the middleware picks up the new
|
||||
// base_url immediately (setup is not complete until base_url is set).
|
||||
setupstatus.Invalidate()
|
||||
}
|
||||
|
||||
// Check for HTMX request
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
// deriveBaseURL returns the base URL to use for constructing self-referential
|
||||
// links (OPDS feeds, sidecar config, etc.). It prefers the database-configured
|
||||
// base_url when available, and falls back to deriving the URL from the incoming
|
||||
// HTTP request (Host header + scheme), which is always reachable by the client.
|
||||
//
|
||||
// Proxy header support: X-Forwarded-Proto and X-Forwarded-Host are respected so
|
||||
// that deployments behind TLS-terminating reverse proxies advertise the correct
|
||||
// external URL.
|
||||
func deriveBaseURL(c *echo.Context, dbBaseURL string) string {
|
||||
if dbBaseURL != "" {
|
||||
return strings.TrimRight(dbBaseURL, "/")
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if c.Request().TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
if proto := c.Request().Header.Get("X-Forwarded-Proto"); proto != "" {
|
||||
scheme = proto
|
||||
}
|
||||
|
||||
host := c.Request().Host
|
||||
if forwarded := c.Request().Header.Get("X-Forwarded-Host"); forwarded != "" {
|
||||
host = forwarded
|
||||
}
|
||||
|
||||
return scheme + "://" + host
|
||||
}
|
||||
+13
-14
@@ -9,7 +9,6 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"bookhoard/internal/config"
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/handlers"
|
||||
"bookhoard/internal/services"
|
||||
@@ -215,6 +214,9 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
bookInfoList = []handlers.BookInfo{}
|
||||
}
|
||||
|
||||
seriesUserUUID, _ := uuid.Parse(user.ID)
|
||||
bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, pgtype.UUID{Bytes: seriesUserUUID, Valid: true}, bookInfoList)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.BrowseDetail(user, "📚", "Series", seriesName, seriesName, "/series", "All Series", "📚", "This series doesn't have any books yet", bookInfoList, errorMsg).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
@@ -267,6 +269,9 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
bookInfoList = []handlers.BookInfo{}
|
||||
}
|
||||
|
||||
tagUserUUID, _ := uuid.Parse(user.ID)
|
||||
bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, pgtype.UUID{Bytes: tagUserUUID, Valid: true}, bookInfoList)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.BrowseDetail(user, "🏷️", "Tag", tagName, tagName, "/bookshelf", "Bookshelf", "🏷️", "No books found with this tag", bookInfoList, errorMsg).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
@@ -354,6 +359,7 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, pgtype.UUID{Bytes: userUUID, Valid: true}, bookInfoList)
|
||||
err = templates.BookShelf(user, libData, libraryID, errorMsg, savedFilters, bookInfoList, limit, offset, totalCount).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -408,7 +414,8 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
// Get only visible sections for the dashboard display
|
||||
visibleSections := cfg.DashboardService.FilterHiddenCollections(allSections, prefs.HiddenCollections)
|
||||
|
||||
sectionData := handlers.BuildSections(visibleSections, libraryID)
|
||||
userPgID := pgtype.UUID{Bytes: userUUID, Valid: true}
|
||||
sectionData := handlers.MarkActiveConflictsSections(c.Request().Context(), cfg.Queries, userPgID, handlers.BuildSections(visibleSections, libraryID))
|
||||
allSectionsData := handlers.BuildSections(allSections, libraryID)
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -623,6 +630,7 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
Description: collection.Description.String,
|
||||
Color: collection.Color.String,
|
||||
Icon: collection.Icon.String,
|
||||
IsSystem: collection.IsSystemCollection.Bool,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -725,10 +733,7 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
}
|
||||
|
||||
// Get base URL from database config with fallback to config/env var
|
||||
baseURL := config.GetBaseURL(c.Request().Context(), cfg.Queries)
|
||||
if baseURL == "" {
|
||||
baseURL = cfg.Cfg.BaseURL
|
||||
}
|
||||
baseURL := cfg.getBaseURL(c.Request().Context())
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.Devices(user, devices, pendingList, errorMsg, baseURL).Render(c.Request().Context(), &buf)
|
||||
@@ -986,10 +991,7 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
}
|
||||
|
||||
// Fetch current system configuration - just base_url
|
||||
baseURL := config.GetBaseURL(c.Request().Context(), cfg.Queries)
|
||||
if baseURL == "" {
|
||||
baseURL = cfg.Cfg.BaseURL
|
||||
}
|
||||
baseURL := cfg.getBaseURL(c.Request().Context())
|
||||
|
||||
systemConfig := map[string]string{
|
||||
"base_url": baseURL,
|
||||
@@ -1077,10 +1079,7 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
}
|
||||
|
||||
// Get base URL from database config with fallback to config/env var
|
||||
baseURL := config.GetBaseURL(c.Request().Context(), cfg.Queries)
|
||||
if baseURL == "" {
|
||||
baseURL = cfg.Cfg.BaseURL
|
||||
}
|
||||
baseURL := cfg.getBaseURL(c.Request().Context())
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.Devices(user, devices, pendingList, errorMsg, baseURL).Render(c.Request().Context(), &buf)
|
||||
|
||||
@@ -72,6 +72,24 @@ type Config struct {
|
||||
LibraryService *services.LibraryService
|
||||
}
|
||||
|
||||
// getBaseURL returns the configured base URL from the database, falling back to
|
||||
// the env var / config default. Uses a closure to adapt the database query to
|
||||
// config.SystemConfigGetter.
|
||||
func (cfg *Config) getBaseURL(ctx context.Context) string {
|
||||
getter := func(ctx context.Context, key string) (string, error) {
|
||||
row, err := cfg.Queries.GetSystemConfig(ctx, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return row.Value, nil
|
||||
}
|
||||
baseURL := config.GetBaseURL(ctx, getter)
|
||||
if baseURL == "" {
|
||||
baseURL = cfg.Cfg.BaseURL
|
||||
}
|
||||
return baseURL
|
||||
}
|
||||
|
||||
// createJWTMiddleware creates a JWT middleware with proper user context setup
|
||||
func createJWTMiddleware(cfg *Config) echo.MiddlewareFunc {
|
||||
return echojwt.WithConfig(echojwt.Config{
|
||||
|
||||
@@ -112,9 +112,15 @@ func handleSearchHTML(c *echo.Context, cfg *Config) error {
|
||||
CoverImagePath: utils.ResolveMediaURL(pgtype.UUID{Bytes: bookLibUUID, Valid: true}, book.CoverImagePath),
|
||||
}
|
||||
}
|
||||
// Render using BooksGrid template
|
||||
// Stamp active conflict flags so cards route the play action correctly
|
||||
bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, user.ID, bookInfoList)
|
||||
// Render using BooksGrid template (or BookPickerGrid for collection picker)
|
||||
var buf bytes.Buffer
|
||||
err = templates.BooksGrid(bookInfoList, limit, offset, totalCount, libraryID).Render(c.Request().Context(), &buf)
|
||||
if c.QueryParam("show_checkbox") == "true" {
|
||||
err = templates.BookPickerGrid(bookInfoList).Render(c.Request().Context(), &buf)
|
||||
} else {
|
||||
err = templates.BooksGrid(bookInfoList, limit, offset, totalCount, libraryID).Render(c.Request().Context(), &buf)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Template render error: %v", err)
|
||||
return c.HTML(http.StatusInternalServerError, `<div style="color: red;">Render error</div>`)
|
||||
|
||||
+40
-10
@@ -14,7 +14,40 @@ import (
|
||||
)
|
||||
|
||||
func isSetupComplete(cfg *Config) bool {
|
||||
return setupstatus.IsSetupComplete(context.Background(), cfg.Queries)
|
||||
getter := func(ctx context.Context) (string, error) {
|
||||
row, err := cfg.Queries.GetSystemConfig(ctx, "base_url")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return row.Value, nil
|
||||
}
|
||||
return setupstatus.IsSetupComplete(context.Background(), cfg.Queries, getter)
|
||||
}
|
||||
|
||||
// setupAllowedAPIRoutes lists API endpoints that remain accessible before
|
||||
// initial setup is complete so the server can be configured via API.
|
||||
var setupAllowedAPIRoutes = []string{
|
||||
"/api/auth/register",
|
||||
"/api/auth/login",
|
||||
"/api/system/config",
|
||||
}
|
||||
|
||||
// isAllowedDuringSetup reports whether a request path should bypass the setup
|
||||
// gate. This includes the setup page itself, static assets, health checks, and
|
||||
// the minimal set of API routes needed to perform initial configuration.
|
||||
func isAllowedDuringSetup(path string) bool {
|
||||
if path == "/setup" || path == "/setup/" {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(path, "/static/") || path == "/health" || path == "/favicon.ico" {
|
||||
return true
|
||||
}
|
||||
for _, route := range setupAllowedAPIRoutes {
|
||||
if path == route || strings.HasPrefix(path, route+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setupRedirectMiddleware(cfg *Config) echo.MiddlewareFunc {
|
||||
@@ -22,19 +55,16 @@ func setupRedirectMiddleware(cfg *Config) echo.MiddlewareFunc {
|
||||
return func(c *echo.Context) error {
|
||||
path := c.Request().URL.Path
|
||||
|
||||
if path == "/setup" || path == "/setup/" {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/api/") {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/static/") || path == "/health" || path == "/favicon.ico" {
|
||||
if isAllowedDuringSetup(path) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
if !isSetupComplete(cfg) {
|
||||
if strings.HasPrefix(path, "/api/") {
|
||||
return c.JSON(http.StatusServiceUnavailable, map[string]string{
|
||||
"error": "Server setup is not complete. Configure an admin account and base_url via the setup wizard or API.",
|
||||
})
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/setup")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Package setupstatus reports whether the application's initial setup has been
|
||||
// completed. Setup is considered complete as soon as at least one admin user
|
||||
// exists, regardless of how that user was created (setup wizard, API, or a
|
||||
// future CLI). This keeps the setup gate a derived property of real data
|
||||
// rather than a manually-flipped flag that can drift out of sync.
|
||||
// completed. Setup is considered complete when at least one admin user exists
|
||||
// AND a non-empty base_url has been configured, regardless of how those were
|
||||
// created (setup wizard, API, or a future CLI). This keeps the setup gate a
|
||||
// derived property of real data rather than a manually-flipped flag that can
|
||||
// drift out of sync.
|
||||
package setupstatus
|
||||
|
||||
import (
|
||||
@@ -18,6 +19,12 @@ type AdminCounter interface {
|
||||
CountAdmins(ctx context.Context) (int64, error)
|
||||
}
|
||||
|
||||
// BaseURLGetter returns the configured base_url value from the database, or an
|
||||
// error if it cannot be read. Defined as a function type (not an interface) so
|
||||
// it can be satisfied by a closure wrapping *database.Queries.GetSystemConfig
|
||||
// without importing the database package.
|
||||
type BaseURLGetter func(ctx context.Context) (string, error)
|
||||
|
||||
var (
|
||||
cacheMu sync.RWMutex
|
||||
cacheComplete bool = true
|
||||
@@ -26,10 +33,11 @@ var (
|
||||
)
|
||||
|
||||
// IsSetupComplete reports whether setup is complete. Setup is complete when at
|
||||
// least one admin user exists. A short in-memory cache avoids hammering the
|
||||
// database on every request. On a database error the function fails open
|
||||
// (returns true) so a transient outage does not lock users out of the app.
|
||||
func IsSetupComplete(ctx context.Context, q AdminCounter) bool {
|
||||
// least one admin user exists AND base_url is configured. A short in-memory
|
||||
// cache avoids hammering the database on every request. On a database error the
|
||||
// function fails open (returns true) so a transient outage does not lock users
|
||||
// out of the app.
|
||||
func IsSetupComplete(ctx context.Context, q AdminCounter, baseURLGetter BaseURLGetter) bool {
|
||||
cacheMu.RLock()
|
||||
if time.Now().Before(cacheExpiry) {
|
||||
complete := cacheComplete
|
||||
@@ -38,12 +46,20 @@ func IsSetupComplete(ctx context.Context, q AdminCounter) bool {
|
||||
}
|
||||
cacheMu.RUnlock()
|
||||
|
||||
count, err := q.CountAdmins(ctx)
|
||||
complete := true
|
||||
|
||||
count, err := q.CountAdmins(ctx)
|
||||
if err == nil {
|
||||
complete = count > 0
|
||||
}
|
||||
|
||||
if complete && baseURLGetter != nil {
|
||||
baseURL, err := baseURLGetter(ctx)
|
||||
if err == nil {
|
||||
complete = baseURL != ""
|
||||
}
|
||||
}
|
||||
|
||||
cacheMu.Lock()
|
||||
cacheComplete = complete
|
||||
cacheExpiry = time.Now().Add(cacheTTL)
|
||||
@@ -53,7 +69,8 @@ func IsSetupComplete(ctx context.Context, q AdminCounter) bool {
|
||||
|
||||
// Invalidate clears the cached setup status so the next call to IsSetupComplete
|
||||
// re-reads from the database. Call this after any write that could change the
|
||||
// admin user count (user creation, role promotion/demotion, user deletion).
|
||||
// admin user count (user creation, role promotion/demotion, user deletion) or
|
||||
// the base_url configuration.
|
||||
func Invalidate() {
|
||||
cacheMu.Lock()
|
||||
cacheComplete = true
|
||||
|
||||
+36
-18
@@ -44,25 +44,43 @@ const config: Config = {
|
||||
],
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: "#7aa2f7",
|
||||
50: "#f0f9ff",
|
||||
100: "#e0f2fe",
|
||||
200: "#bae6fd",
|
||||
300: "#7dd3fc",
|
||||
400: "#38bdf8",
|
||||
500: "#0ea5e9",
|
||||
600: "#0284c7",
|
||||
700: "#0369a1",
|
||||
800: "#075985",
|
||||
900: "#0c4a6e",
|
||||
// Semantic surface tokens (page bg, cards, raised layers)
|
||||
surface: {
|
||||
DEFAULT: "var(--bg-primary)",
|
||||
raised: "var(--bg-secondary)",
|
||||
hover: "var(--surface-hover)",
|
||||
overlay: "var(--surface-overlay)",
|
||||
},
|
||||
"bg-primary": "var(--bg-primary)",
|
||||
"bg-secondary": "var(--bg-secondary)",
|
||||
"text-primary": "var(--text-primary)",
|
||||
"text-secondary": "var(--text-secondary)",
|
||||
accent: "var(--accent)",
|
||||
border: "var(--border)",
|
||||
// Text tokens
|
||||
content: {
|
||||
DEFAULT: "var(--text-primary)",
|
||||
muted: "var(--text-secondary)",
|
||||
},
|
||||
// Accent / brand
|
||||
brand: {
|
||||
DEFAULT: "var(--accent)",
|
||||
muted: "var(--accent-muted)",
|
||||
},
|
||||
// Borders / hairlines
|
||||
line: {
|
||||
DEFAULT: "var(--border)",
|
||||
strong: "var(--border-strong)",
|
||||
},
|
||||
// Status colors (theme-aware via vars, fall back to fixed)
|
||||
success: "var(--status-success)",
|
||||
warning: "var(--status-warning)",
|
||||
danger: "var(--status-danger)",
|
||||
info: "var(--status-info)",
|
||||
},
|
||||
borderRadius: {
|
||||
xl: "0.875rem",
|
||||
"2xl": "1.25rem",
|
||||
},
|
||||
boxShadow: {
|
||||
card: "var(--shadow-card)",
|
||||
"card-hover": "var(--shadow-card-hover)",
|
||||
pop: "var(--shadow-pop)",
|
||||
bar: "var(--shadow-bar)",
|
||||
},
|
||||
backgroundImage: {
|
||||
"wood-light": "url('/static/textures/wood-light.png')",
|
||||
|
||||
+54
-32
@@ -11,60 +11,79 @@ templ Admin(user User) {
|
||||
</head>
|
||||
<body x-data="admin" x-init="loadWatchStatus(); initializeScanWebSocket()" class="theme-tokyo-night">
|
||||
@Header(user, "/admin")
|
||||
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
|
||||
<div class="flex">
|
||||
@AdminSidebar(user, "/admin")
|
||||
<main class="flex-1 p-8">
|
||||
<div class="max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Dashboard</h1>
|
||||
<p style="color: var(--text-secondary)">Overview of your Bookhoard library and settings</p>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("grid", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Dashboard</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Overview of your Bookhoard library and settings</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="text-3xl">📖</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="grid place-items-center h-11 w-11 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("library", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h3 class="font-semibold" style="color: var(--text-primary)">Library</h3>
|
||||
<p style="color: var(--text-secondary)" class="text-sm">Manage your ebook collection</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Manage your ebook collection</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/" class="mt-4 inline-block text-sm btn-secondary px-3 py-1 rounded">View Library</a>
|
||||
<a href="/" class="btn btn-secondary mt-4 text-sm">
|
||||
@Icon("arrow-right", "h-4 w-4")
|
||||
View Library
|
||||
</a>
|
||||
</div>
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="text-3xl">👁️</div>
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="grid place-items-center h-11 w-11 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("sync", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h3 class="font-semibold" style="color: var(--text-primary)">Scan Watch Status</h3>
|
||||
<p style="color: var(--text-secondary)" class="text-sm">Auto-detecting new files</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Auto-detecting new files</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="watch-status" class="mt-4 text-sm" style="color: var(--text-secondary)">
|
||||
<span class="inline-block w-2 h-2 rounded-full bg-green-500 mr-2"></span>
|
||||
<span class="inline-block w-2 h-2 rounded-full mr-2" style="background-color: var(--status-success);"></span>
|
||||
Watching <span id="watch-count">0</span> libraries
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Quick Actions</h3>
|
||||
<div class="card p-6">
|
||||
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Quick Actions</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<button @click="scanAllLibraries()" class="btn-primary p-4 rounded-lg text-left">
|
||||
<div class="font-medium">Rescan Library</div>
|
||||
<div style="color: var(--text-secondary)" class="text-sm">Re-scan existing files and fix metadata</div>
|
||||
<button @click="scanAllLibraries()" class="btn btn-primary py-4 flex-col items-start gap-1">
|
||||
<span class="flex items-center gap-2 font-medium">
|
||||
@Icon("refresh", "h-5 w-5")
|
||||
Rescan Library
|
||||
</span>
|
||||
<span class="text-xs font-normal opacity-80">Re-scan existing files and fix metadata</span>
|
||||
</button>
|
||||
<a href="/admin/library" class="btn-secondary p-4 rounded-lg text-left block">
|
||||
<div class="font-medium">Manage Libraries and Folders</div>
|
||||
<div style="color: var(--text-secondary)" class="text-sm">Add or remove libraries and scan directories</div>
|
||||
<a href="/admin/library" class="btn btn-secondary py-4 flex-col items-start gap-1">
|
||||
<span class="flex items-center gap-2 font-medium">
|
||||
@Icon("library", "h-5 w-5")
|
||||
Manage Libraries
|
||||
</span>
|
||||
<span class="text-xs font-normal opacity-80">Add or remove libraries and scan directories</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Scan Progress Section -->
|
||||
<div id="scan-progress-container" class="hidden mt-6 p-6 rounded-lg border opacity-0 -translate-y-2.5 transition-all duration-300 ease-out" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div id="scan-progress-container" class="card hidden mt-6 p-6 opacity-0 -translate-y-2.5 transition-all duration-300 ease-out">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">
|
||||
📚 Scanning Libraries
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2" style="color: var(--text-primary)">
|
||||
@Icon("refresh", "h-5 w-5")
|
||||
Scanning Libraries
|
||||
</h3>
|
||||
<button @click="hideScanProgress()" class="p-2 hover:bg-gray-700 rounded">
|
||||
✕
|
||||
<button @click="hideScanProgress()" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<!-- Overall Progress -->
|
||||
@@ -73,7 +92,7 @@ templ Admin(user User) {
|
||||
<span style="color: var(--text-secondary)">Overall Progress</span>
|
||||
<span id="scan-progress-text" style="color: var(--text-primary)">0%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-3">
|
||||
<div class="w-full rounded-full h-3" style="background-color: var(--surface-hover);">
|
||||
<div
|
||||
id="scan-progress-bar"
|
||||
class="h-3 rounded-full transition-all duration-500"
|
||||
@@ -89,21 +108,24 @@ templ Admin(user User) {
|
||||
<!-- Dynamically populated -->
|
||||
</div>
|
||||
<!-- Results Summary -->
|
||||
<div id="scan-results" class="hidden mt-6 p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||
<h4 class="font-semibold mb-2" style="color: var(--text-primary)">✅ Scan Complete!</h4>
|
||||
<div id="scan-results" class="hidden mt-6 p-4 rounded-xl border" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||
<h4 class="font-semibold mb-2 flex items-center gap-2" style="color: var(--status-success);">
|
||||
@Icon("check-circle", "h-5 w-5")
|
||||
Scan Complete!
|
||||
</h4>
|
||||
<div id="scan-results-content" style="color: var(--text-secondary)">
|
||||
<!-- Results populated by JS -->
|
||||
</div>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<button
|
||||
@click="window.location.reload()"
|
||||
class="btn-primary px-4 py-2 rounded-lg"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Refresh to View Books
|
||||
</button>
|
||||
<button
|
||||
@click="hideScanProgress()"
|
||||
class="btn-secondary px-4 py-2 rounded-lg"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
|
||||
@@ -10,49 +10,69 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
|
||||
</head>
|
||||
<body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }">
|
||||
@Header(user, "/admin/library")
|
||||
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
|
||||
<div class="flex">
|
||||
@AdminSidebar(user, "/admin/library")
|
||||
<main class="flex-1 p-8">
|
||||
<div class="w-full">
|
||||
<div class="max-w-5xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<a href="/admin" class="btn-secondary px-4 py-2 rounded-lg font-medium">
|
||||
← Back to Dashboard
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap mb-4">
|
||||
<a href="/admin" class="btn btn-secondary">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<button data-action="show-create-modal" class="btn-primary px-4 py-2 rounded-lg font-medium">
|
||||
+ Create Library
|
||||
<button data-action="show-create-modal" class="btn btn-primary">
|
||||
@Icon("plus", "h-4 w-4")
|
||||
Create Library
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Library Management</h1>
|
||||
<p style="color: var(--text-secondary)">Manage libraries and configure media scanning</p>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("library", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Library Management</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Manage libraries and configure media scanning</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Libraries Section -->
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Libraries</h3>
|
||||
<p style="color: var(--text-secondary)" class="mb-4">Manage media libraries and their folders</p>
|
||||
<div id="libraries-list" class="space-y-3 mb-6">
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
@Icon("library", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Libraries</h3>
|
||||
</div>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">Manage media libraries and their folders</p>
|
||||
<div id="libraries-list" class="space-y-3 mb-2">
|
||||
if len(libraries) == 0 {
|
||||
<p style="color: var(--text-secondary)" class="text-center py-8">
|
||||
<p class="text-center py-8 text-sm" style="color: var(--text-secondary)">
|
||||
No libraries yet. Create your first library to get started.
|
||||
</p>
|
||||
} else {
|
||||
for _, library := range libraries {
|
||||
<div class="p-4 border rounded-lg" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<div class="p-4 rounded-xl border transition-colors hover:bg-surface-hover" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<div class="flex justify-between items-start gap-3 mb-2">
|
||||
<div class="min-w-0">
|
||||
<h4 class="font-semibold" style="color: var(--text-primary)">{ library.Name }</h4>
|
||||
if library.Description != "" {
|
||||
<p class="text-sm" style="color: var(--text-secondary)">{ library.Description }</p>
|
||||
}
|
||||
<span class="inline-block px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)">
|
||||
<span class="chip mt-1">
|
||||
@Icon("tag", "h-3 w-3")
|
||||
{ library.TypeName }
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button data-library-id={ library.ID } data-action="show-folders" class="text-xs px-2 py-1 rounded" style="background-color: var(--bg-secondary); color: var(--text-primary)">Folders</button>
|
||||
<button data-library-id={ library.ID } data-action="edit" class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: var(--bg-primary)">Edit</button>
|
||||
<button data-library-id={ library.ID } data-action="delete" class="text-xs px-2 py-1 rounded text-red-500">Delete</button>
|
||||
<div class="flex flex-wrap gap-1 shrink-0">
|
||||
<button data-library-id={ library.ID } data-action="show-folders" class="btn btn-secondary text-xs px-2.5 py-1">
|
||||
@Icon("folder", "h-4 w-4")
|
||||
Folders
|
||||
</button>
|
||||
<button data-library-id={ library.ID } data-action="edit" class="btn btn-primary text-xs px-2.5 py-1">
|
||||
@Icon("edit", "h-4 w-4")
|
||||
Edit
|
||||
</button>
|
||||
<button data-library-id={ library.ID } data-action="delete" class="btn btn-danger text-xs px-2.5 py-1">
|
||||
@Icon("trash", "h-4 w-4")
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id={ "library-folders-" + library.ID } class="hidden mt-3 space-y-2"></div>
|
||||
@@ -62,20 +82,26 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
|
||||
</div>
|
||||
</div>
|
||||
<!-- User Library Visibility Section -->
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Library Visibility</h3>
|
||||
<p style="color: var(--text-secondary)" class="mb-4">Control which libraries are visible to users</p>
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
@Icon("check-circle", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Library Visibility</h3>
|
||||
</div>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">Control which libraries are visible to users</p>
|
||||
<div id="visibility-controls" class="space-y-4">
|
||||
<!-- Visibility controls will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- User Visibility Management -->
|
||||
<div class="mt-8 card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">User Library Access</h3>
|
||||
<p style="color: var(--text-secondary)" class="mb-4">Manage individual user access to specific libraries</p>
|
||||
<div class="mt-6 card p-6">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
@Icon("users", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">User Library Access</h3>
|
||||
</div>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">Manage individual user access to specific libraries</p>
|
||||
<div class="mb-4">
|
||||
<select id="user-select" onchange="loadUserVisibility()" class="px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
|
||||
<select id="user-select" onchange="loadUserVisibility()" class="input w-auto">
|
||||
<option value="">Select a user...</option>
|
||||
for _, user := range users {
|
||||
<option value="{ user.ID }">{ user.Username } ({ user.Email })</option>
|
||||
@@ -90,43 +116,50 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
|
||||
</main>
|
||||
</div>
|
||||
<!-- Create Library Modal -->
|
||||
<div id="create-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div id="create-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Library</h2>
|
||||
<button type="button" data-action="hide-create-modal" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<button type="button" data-action="hide-create-modal" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<form id="create-library-form">
|
||||
<input type="hidden" id="library-id" name="id"/>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Library Name</label>
|
||||
<input type="text" name="name" placeholder="My Ebook Library" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required/>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Name</label>
|
||||
<input type="text" name="name" placeholder="My Ebook Library" class="input" required/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Description</label>
|
||||
<textarea name="description" placeholder="Optional description" rows="3" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"></textarea>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Description</label>
|
||||
<textarea name="description" placeholder="Optional description" rows="3" class="input"></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Library Type</label>
|
||||
<select name="type" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
||||
<option value="ebooks">📚 Ebooks</option>
|
||||
<option value="comics">📖 Comics</option>
|
||||
<option value="manga">🗾 Manga</option>
|
||||
<div class="mb-6">
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Type</label>
|
||||
<select name="type" class="input" required>
|
||||
<option value="ebooks">Ebooks</option>
|
||||
<option value="comics">Comics</option>
|
||||
<option value="manga">Manga</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" data-action="hide-create-modal" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">Create</button>
|
||||
<button type="button" data-action="hide-create-modal" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@Icon("plus", "h-4 w-4")
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Folder Browser Modal -->
|
||||
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
|
||||
<button type="button" data-action="browse-cancel" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<button type="button" data-action="browse-cancel" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<div id="folder-browser-content">
|
||||
<!-- Directory listings will be rendered here -->
|
||||
@@ -134,18 +167,23 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete Library Confirmation Modal -->
|
||||
<div id="delete-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div id="delete-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Delete Library</h2>
|
||||
<button type="button" data-action="hide-delete-modal" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<button type="button" data-action="hide-delete-modal" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<div id="delete-modal-content" class="mb-6" style="color: var(--text-primary)">
|
||||
<!-- Dynamic content will be injected here -->
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" data-action="hide-delete-modal" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button>
|
||||
<button type="button" data-action="confirm-delete" class="btn-primary px-4 py-2 rounded-lg bg-red-500 hover:bg-red-600">Delete</button>
|
||||
<button type="button" data-action="hide-delete-modal" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" data-action="confirm-delete" class="btn btn-danger">
|
||||
@Icon("trash", "h-4 w-4")
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User) templ.Compon
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex min-h-screen\" style=\"background-color: var(--bg-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -45,161 +45,277 @@ func AdminLibrary(user User, libraries []LibraryData, users []User) templ.Compon
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"w-full\"><div class=\"mb-8\"><div class=\"flex items-center justify-between mb-4\"><a href=\"/admin\" class=\"btn-secondary px-4 py-2 rounded-lg font-medium\">← Back to Dashboard</a> <button data-action=\"show-create-modal\" class=\"btn-primary px-4 py-2 rounded-lg font-medium\">+ Create Library</button></div><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">Library Management</h1><p style=\"color: var(--text-secondary)\">Manage libraries and configure media scanning</p></div><div class=\"grid grid-cols-1 lg:grid-cols-2 gap-8\"><!-- Libraries Section --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h3 class=\"text-xl font-semibold mb-6\" style=\"color: var(--text-primary)\">Libraries</h3><p style=\"color: var(--text-secondary)\" class=\"mb-4\">Manage media libraries and their folders</p><div id=\"libraries-list\" class=\"space-y-3 mb-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-5xl\"><div class=\"mb-8\"><div class=\"flex items-center justify-between gap-4 flex-wrap mb-4\"><a href=\"/admin\" class=\"btn btn-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Back to Dashboard</a> <button data-action=\"show-create-modal\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Create Library</button></div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("library", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Library Management</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Manage libraries and configure media scanning</p></div><div class=\"grid grid-cols-1 lg:grid-cols-2 gap-6\"><!-- Libraries Section --><div class=\"card p-6\"><div class=\"flex items-center gap-2 mb-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("library", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Libraries</h3></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Manage media libraries and their folders</p><div id=\"libraries-list\" class=\"space-y-3 mb-2\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(libraries) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<p style=\"color: var(--text-secondary)\" class=\"text-center py-8\">No libraries yet. Create your first library to get started.</p>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p class=\"text-center py-8 text-sm\" style=\"color: var(--text-secondary)\">No libraries yet. Create your first library to get started.</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
for _, library := range libraries {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"p-4 border rounded-lg\" style=\"background-color: var(--bg-primary); border-color: var(--border)\"><div class=\"flex justify-between items-start mb-2\"><div><h4 class=\"font-semibold\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"p-4 rounded-xl border transition-colors hover:bg-surface-hover\" style=\"background-color: var(--bg-primary); border-color: var(--border)\"><div class=\"flex justify-between items-start gap-3 mb-2\"><div class=\"min-w-0\"><h4 class=\"font-semibold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(library.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 44, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 54, Col: 89}
|
||||
}
|
||||
_, 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, 6, "</h4>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</h4>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if library.Description != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(library.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 46, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 56, Col: 92}
|
||||
}
|
||||
_, 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, 8, "</p>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"inline-block px-2 py-1 text-xs rounded\" style=\"background-color: var(--accent); color: var(--bg-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"chip mt-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("tag", "h-3 w-3").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(library.TypeName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 49, Col: 33}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 60, Col: 33}
|
||||
}
|
||||
_, 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, "</span></div><div class=\"flex space-x-2\"><button data-library-id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></div><div class=\"flex flex-wrap gap-1 shrink-0\"><button data-library-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 53, Col: 50}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 64, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-action=\"show-folders\" class=\"text-xs px-2 py-1 rounded\" style=\"background-color: var(--bg-secondary); color: var(--text-primary)\">Folders</button> <button data-library-id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" data-action=\"show-folders\" class=\"btn btn-secondary text-xs px-2.5 py-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("folder", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Folders</button> <button data-library-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 54, Col: 50}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 68, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" data-action=\"edit\" class=\"text-xs px-2 py-1 rounded\" style=\"background-color: var(--accent); color: var(--bg-primary)\">Edit</button> <button data-library-id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" data-action=\"edit\" class=\"btn btn-primary text-xs px-2.5 py-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("edit", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Edit</button> <button data-library-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 55, Col: 50}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 72, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" data-action=\"delete\" class=\"text-xs px-2 py-1 rounded text-red-500\">Delete</button></div></div><div id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" data-action=\"delete\" class=\"btn btn-danger text-xs px-2.5 py-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "Delete</button></div></div><div id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("library-folders-" + library.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 58, Col: 53}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 78, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"hidden mt-3 space-y-2\"></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"hidden mt-3 space-y-2\"></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></div><!-- User Library Visibility Section --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h3 class=\"text-xl font-semibold mb-6\" style=\"color: var(--text-primary)\">Library Visibility</h3><p style=\"color: var(--text-secondary)\" class=\"mb-4\">Control which libraries are visible to users</p><div id=\"visibility-controls\" class=\"space-y-4\"><!-- Visibility controls will be loaded here --></div></div></div><!-- User Visibility Management --><div class=\"mt-8 card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h3 class=\"text-xl font-semibold mb-6\" style=\"color: var(--text-primary)\">User Library Access</h3><p style=\"color: var(--text-secondary)\" class=\"mb-4\">Manage individual user access to specific libraries</p><div class=\"mb-4\"><select id=\"user-select\" onchange=\"loadUserVisibility()\" class=\"px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\"><option value=\"\">Select a user...</option> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div><!-- User Library Visibility Section --><div class=\"card p-6\"><div class=\"flex items-center gap-2 mb-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("check-circle", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Library Visibility</h3></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Control which libraries are visible to users</p><div id=\"visibility-controls\" class=\"space-y-4\"><!-- Visibility controls will be loaded here --></div></div></div><!-- User Visibility Management --><div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("users", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">User Library Access</h3></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Manage individual user access to specific libraries</p><div class=\"mb-4\"><select id=\"user-select\" onchange=\"loadUserVisibility()\" class=\"input w-auto\"><option value=\"\">Select a user...</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, user := range users {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<option value=\"{ user.ID }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<option value=\"{ user.ID }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 81, Col: 53}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 107, Col: 53}
|
||||
}
|
||||
_, 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, 17, " (")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " (")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 81, Col: 69}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 107, Col: 69}
|
||||
}
|
||||
_, 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, 18, ")</option>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, ")</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</select></div><div id=\"user-libraries\" class=\"space-y-3\"><!-- User library checkboxes will be loaded here --></div></div></div></main></div><!-- Create Library Modal --><div id=\"create-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Create Library</h2><button type=\"button\" data-action=\"hide-create-modal\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><form id=\"create-library-form\"><input type=\"hidden\" id=\"library-id\" name=\"id\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-primary)\">Library Name</label> <input type=\"text\" name=\"name\" placeholder=\"My Ebook Library\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-primary)\">Description</label> <textarea name=\"description\" placeholder=\"Optional description\" rows=\"3\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\"></textarea></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-primary)\">Library Type</label> <select name=\"type\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required><option value=\"ebooks\">📚 Ebooks</option> <option value=\"comics\">📖 Comics</option> <option value=\"manga\">🗾 Manga</option></select></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" data-action=\"hide-create-modal\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Create</button></div></form></div></div><!-- Folder Browser Modal --><div id=\"folder-browser-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Browse Folders</h2><button type=\"button\" data-action=\"browse-cancel\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><div id=\"folder-browser-content\"><!-- Directory listings will be rendered here --></div></div></div><!-- Delete Library Confirmation Modal --><div id=\"delete-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Delete Library</h2><button type=\"button\" data-action=\"hide-delete-modal\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><div id=\"delete-modal-content\" class=\"mb-6\" style=\"color: var(--text-primary)\"><!-- Dynamic content will be injected here --></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" data-action=\"hide-delete-modal\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"button\" data-action=\"confirm-delete\" class=\"btn-primary px-4 py-2 rounded-lg bg-red-500 hover:bg-red-600\">Delete</button></div></div></div><script src=\"/static/htmx.min.js\"></script></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</select></div><div id=\"user-libraries\" class=\"space-y-3\"><!-- User library checkboxes will be loaded here --></div></div></div></main></div><!-- Create Library Modal --><div id=\"create-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md mx-4\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Create Library</h2><button type=\"button\" data-action=\"hide-create-modal\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</button></div><form id=\"create-library-form\"><input type=\"hidden\" id=\"library-id\" name=\"id\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Library Name</label> <input type=\"text\" name=\"name\" placeholder=\"My Ebook Library\" class=\"input\" required></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" placeholder=\"Optional description\" rows=\"3\" class=\"input\"></textarea></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Library Type</label> <select name=\"type\" class=\"input\" required><option value=\"ebooks\">Ebooks</option> <option value=\"comics\">Comics</option> <option value=\"manga\">Manga</option></select></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" data-action=\"hide-create-modal\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Create</button></div></form></div></div><!-- Folder Browser Modal --><div id=\"folder-browser-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md mx-4\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Browse Folders</h2><button type=\"button\" data-action=\"browse-cancel\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</button></div><div id=\"folder-browser-content\"><!-- Directory listings will be rendered here --></div></div></div><!-- Delete Library Confirmation Modal --><div id=\"delete-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md mx-4\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Delete Library</h2><button type=\"button\" data-action=\"hide-delete-modal\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</button></div><div id=\"delete-modal-content\" class=\"mb-6\" style=\"color: var(--text-primary)\"><!-- Dynamic content will be injected here --></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" data-action=\"hide-delete-modal\" class=\"btn btn-secondary\">Cancel</button> <button type=\"button\" data-action=\"confirm-delete\" class=\"btn btn-danger\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "Delete</button></div></div></div><script src=\"/static/htmx.min.js\"></script></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
package templates
|
||||
|
||||
templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssueData, stats IssueStats) {
|
||||
@@ -12,83 +11,105 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
|
||||
<body x-data="processingIssues" x-init="initializeProcessingIssues('{ libraryID }')" class="theme-{ user.Theme }">
|
||||
@Header(user, "/admin/libraries/"+libraryID)
|
||||
<main class="flex-1 p-8">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-2">Processing Issues</h1>
|
||||
<p class="text-gray-600">Items that couldn't be processed in this library</p>
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap mb-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("alert", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Processing Issues</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Items that couldn't be processed in this library</p>
|
||||
</div>
|
||||
<a href="/admin/libraries/{ libraryID }" class="btn btn-secondary">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
Back to Library
|
||||
</a>
|
||||
</div>
|
||||
<a href="/admin/libraries/{ libraryID }" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
← Back to Library
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 {
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
if stats.ErrorCount > 0 {
|
||||
<div class="card p-6 rounded-lg border-l-4 border-red-500">
|
||||
<h3 class="text-lg font-semibold text-red-600 mb-2">Errors</h3>
|
||||
<p class="text-3xl font-bold">{ stats.ErrorCount }</p>
|
||||
</div>
|
||||
}
|
||||
if stats.WarningCount > 0 {
|
||||
<div class="card p-6 rounded-lg border-l-4 border-yellow-500">
|
||||
<h3 class="text-lg font-semibold text-yellow-600 mb-2">Warnings</h3>
|
||||
<p class="text-3xl font-bold">{ stats.WarningCount }</p>
|
||||
</div>
|
||||
}
|
||||
if stats.InfoCount > 0 {
|
||||
<div class="card p-6 rounded-lg border-l-4 border-blue-500">
|
||||
<h3 class="text-lg font-semibold text-blue-600 mb-2">Info</h3>
|
||||
<p class="text-3xl font-bold">{ stats.InfoCount }</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
<div class="card p-8 rounded-lg text-center">
|
||||
<p class="text-gray-600">No processing issues found for this library.</p>
|
||||
</div>
|
||||
} else {
|
||||
<!-- Issues List -->
|
||||
<div class="space-y-4">
|
||||
for _, issue := range issues {
|
||||
<div class="card p-6 rounded-lg">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="flex-1">
|
||||
<h4 class="text-lg font-semibold mb-2">{ issue.Title }</h4>
|
||||
<p class="text-gray-700 mb-3">{ issue.IssueDescription }</p>
|
||||
<div class="text-sm text-gray-500 space-y-1">
|
||||
<p><strong>Type:</strong> { issue.IssueType }</p>
|
||||
<p><strong>Format:</strong> { issue.FormatGroup }</p>
|
||||
<p><strong>File:</strong> { issue.FilePath }</p>
|
||||
<p><strong>Library:</strong> { issue.LibraryTypeName }</p>
|
||||
if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 {
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
if stats.ErrorCount > 0 {
|
||||
<div class="stat-card" style="border-left: 4px solid var(--status-danger);">
|
||||
<div class="flex items-center gap-2 mb-2" style="color: var(--status-danger);">
|
||||
@Icon("x-circle", "h-5 w-5")
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide">Errors</h3>
|
||||
</div>
|
||||
<p class="text-3xl font-bold" style="color: var(--text-primary)">{ stats.ErrorCount }</p>
|
||||
</div>
|
||||
}
|
||||
if stats.WarningCount > 0 {
|
||||
<div class="stat-card" style="border-left: 4px solid var(--status-warning);">
|
||||
<div class="flex items-center gap-2 mb-2" style="color: var(--status-warning);">
|
||||
@Icon("alert", "h-5 w-5")
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide">Warnings</h3>
|
||||
</div>
|
||||
<p class="text-3xl font-bold" style="color: var(--text-primary)">{ stats.WarningCount }</p>
|
||||
</div>
|
||||
}
|
||||
if stats.InfoCount > 0 {
|
||||
<div class="stat-card" style="border-left: 4px solid var(--status-info);">
|
||||
<div class="flex items-center gap-2 mb-2" style="color: var(--status-info);">
|
||||
@Icon("info", "h-5 w-5")
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide">Info</h3>
|
||||
</div>
|
||||
<p class="text-3xl font-bold" style="color: var(--text-primary)">{ stats.InfoCount }</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
<div class="card p-8 text-center">
|
||||
<span class="grid place-items-center h-12 w-12 mx-auto mb-3 rounded-2xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("check-circle", "h-6 w-6")
|
||||
</span>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">No processing issues found for this library.</p>
|
||||
</div>
|
||||
} else {
|
||||
<!-- Issues List -->
|
||||
<div class="space-y-4">
|
||||
for _, issue := range issues {
|
||||
<div class="card p-6">
|
||||
<div class="flex justify-between items-start gap-4 mb-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ issue.Title }</h4>
|
||||
<p class="mb-3 text-sm" style="color: var(--text-secondary)">{ issue.IssueDescription }</p>
|
||||
<div class="text-sm space-y-1" style="color: var(--text-secondary)">
|
||||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Type:</span> { issue.IssueType }</p>
|
||||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Format:</span> { issue.FormatGroup }</p>
|
||||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">File:</span> { issue.FilePath }</p>
|
||||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Library:</span> { issue.LibraryTypeName }</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-2 shrink-0">
|
||||
if issue.Severity == "error" {
|
||||
<span class="badge status-failed">{ issue.Severity }</span>
|
||||
} else if issue.Severity == "warning" {
|
||||
<span class="badge status-pending">{ issue.Severity }</span>
|
||||
} else {
|
||||
<span class="badge status-processing">{ issue.Severity }</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<span
|
||||
class="inline-block px-3 py-1 text-sm rounded-full font-medium"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
>
|
||||
{ issue.Severity }
|
||||
</span>
|
||||
<div class="flex gap-3 mt-4">
|
||||
if issue.Severity == "warning" || issue.Severity == "info" {
|
||||
<button
|
||||
@click="dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')"
|
||||
class="btn btn-secondary text-sm"
|
||||
>
|
||||
@Icon("close", "h-4 w-4")
|
||||
Dismiss
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-4">
|
||||
if issue.Severity == "warning" || issue.Severity == "info" {
|
||||
<button
|
||||
@click="dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')"
|
||||
class="btn-secondary px-4 py-2 rounded text-sm"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
|
||||
package templates
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
@@ -38,200 +37,302 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"flex-1 p-8\"><div class=\"mb-8\"><div class=\"flex items-center justify-between mb-4\"><div><h1 class=\"text-3xl font-bold mb-2\">Processing Issues</h1><p class=\"text-gray-600\">Items that couldn't be processed in this library</p></div><a href=\"/admin/libraries/{ libraryID }\" class=\"btn-secondary px-4 py-2 rounded-lg\">← Back to Library</a></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"flex-1 p-8\"><div class=\"mx-auto max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center justify-between gap-4 flex-wrap mb-4\"><div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("alert", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Processing Issues</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Items that couldn't be processed in this library</p></div><a href=\"/admin/libraries/{ libraryID }\" class=\"btn btn-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Back to Library</a></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<!-- Stats Cards --> <div class=\"grid grid-cols-1 md:grid-cols-3 gap-6 mb-8\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<!-- Stats Cards --> <div class=\"grid grid-cols-1 md:grid-cols-3 gap-6 mb-8\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if stats.ErrorCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"card p-6 rounded-lg border-l-4 border-red-500\"><h3 class=\"text-lg font-semibold text-red-600 mb-2\">Errors</h3><p class=\"text-3xl font-bold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-danger);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-danger);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("x-circle", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Errors</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.ErrorCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 32, Col: 56}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 41, Col: 92}
|
||||
}
|
||||
_, 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>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if stats.WarningCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"card p-6 rounded-lg border-l-4 border-yellow-500\"><h3 class=\"text-lg font-semibold text-yellow-600 mb-2\">Warnings</h3><p class=\"text-3xl font-bold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-warning);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-warning);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("alert", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Warnings</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.WarningCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 38, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 50, Col: 94}
|
||||
}
|
||||
_, 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, 7, "</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if stats.InfoCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"card p-6 rounded-lg border-l-4 border-blue-500\"><h3 class=\"text-lg font-semibold text-blue-600 mb-2\">Info</h3><p class=\"text-3xl font-bold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-info);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-info);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("info", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Info</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.InfoCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 44, Col: 55}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 59, Col: 91}
|
||||
}
|
||||
_, 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, 9, "</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"card p-8 rounded-lg text-center\"><p class=\"text-gray-600\">No processing issues found for this library.</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"card p-8 text-center\"><span class=\"grid place-items-center h-12 w-12 mx-auto mb-3 rounded-2xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("check-circle", "h-6 w-6").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span><p class=\"text-sm\" style=\"color: var(--text-secondary)\">No processing issues found for this library.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<!-- Issues List --> <div class=\"space-y-4\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<!-- Issues List --> <div class=\"space-y-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, issue := range issues {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"card p-6 rounded-lg\"><div class=\"flex justify-between items-start mb-4\"><div class=\"flex-1\"><h4 class=\"text-lg font-semibold mb-2\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"card p-6\"><div class=\"flex justify-between items-start gap-4 mb-4\"><div class=\"flex-1 min-w-0\"><h4 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 60, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 78, Col: 98}
|
||||
}
|
||||
_, 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, 14, "</h4><p class=\"text-gray-700 mb-3\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</h4><p class=\"mb-3 text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueDescription)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 61, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 79, Col: 96}
|
||||
}
|
||||
_, 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, "</p><div class=\"text-sm text-gray-500 space-y-1\"><p><strong>Type:</strong> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</p><div class=\"text-sm space-y-1\" style=\"color: var(--text-secondary)\"><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Type:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 63, Col: 54}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 81, Col: 140}
|
||||
}
|
||||
_, 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, "</p><p><strong>Format:</strong> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Format:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FormatGroup)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 64, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 82, Col: 144}
|
||||
}
|
||||
_, 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, "</p><p><strong>File:</strong> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">File:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FilePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 65, Col: 53}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 83, Col: 139}
|
||||
}
|
||||
_, 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, 18, "</p><p><strong>Library:</strong> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Library:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(issue.LibraryTypeName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 66, Col: 63}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 84, Col: 149}
|
||||
}
|
||||
_, 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, 19, "</p></div></div><div class=\"ml-4\"><span class=\"inline-block px-3 py-1 text-sm rounded-full font-medium\" style=\"background-color: var(--accent); color: white;\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</p></div></div><div class=\"ml-2 shrink-0\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 74, Col: 27}
|
||||
}
|
||||
_, 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, 20, "</span></div></div><div class=\"flex gap-3 mt-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if issue.Severity == "warning" || issue.Severity == "info" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<button @click=\"dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')\" class=\"btn-secondary px-4 py-2 rounded text-sm\">Dismiss</button>")
|
||||
if issue.Severity == "error" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<span class=\"badge status-failed\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 89, Col: 62}
|
||||
}
|
||||
_, 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, 27, "</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if issue.Severity == "warning" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<span class=\"badge status-pending\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 91, Col: 63}
|
||||
}
|
||||
_, 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, 29, "</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<span class=\"badge status-processing\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 93, Col: 66}
|
||||
}
|
||||
_, 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, 31, "</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div></div><div class=\"flex gap-3 mt-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if issue.Severity == "warning" || issue.Severity == "info" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<button @click=\"dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')\" class=\"btn btn-secondary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Dismiss</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</main></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div></main></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -11,55 +11,67 @@ templ AdminSettings(user User, systemConfig map[string]string, errorMessage stri
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }" x-data="adminSettings">
|
||||
@Header(user, "/admin/settings")
|
||||
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
|
||||
<div class="flex">
|
||||
@AdminSidebar(user, "/admin/settings")
|
||||
<main class="flex-1 p-8">
|
||||
<div class="max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<a href="/admin" class="btn-secondary px-4 py-2 rounded-lg font-medium">
|
||||
← Back to Dashboard
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap mb-4">
|
||||
<a href="/admin" class="btn btn-secondary">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">System Settings</h1>
|
||||
<p style="color: var(--text-secondary)">Configure your Bookhoard instance</p>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("settings", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">System Settings</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Configure your Bookhoard instance</p>
|
||||
</div>
|
||||
if errorMessage != "" {
|
||||
<div class="mb-6 p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--error); color: var(--error);">
|
||||
{ errorMessage }
|
||||
<div class="mb-6 p-4 rounded-xl border flex items-start gap-3" style="background-color: color-mix(in srgb, var(--status-danger) 12%, var(--bg-secondary)); border-color: var(--status-danger); color: var(--status-danger);">
|
||||
@Icon("alert", "h-5 w-5 shrink-0 mt-0.5")
|
||||
<span>{ errorMessage }</span>
|
||||
</div>
|
||||
}
|
||||
<form id="settings-form" hx-put="/api/system/config" hx-target="#settings-form" hx-swap="outerHTML">
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Base URL</h3>
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
@Icon("globe", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Base URL</h3>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Base URL</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Base URL</label>
|
||||
<input
|
||||
type="url"
|
||||
name="base_url"
|
||||
value={ systemConfig["base_url"] }
|
||||
placeholder="https://books.example.com"
|
||||
class="w-full px-4 py-2 rounded-lg border"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
required
|
||||
/>
|
||||
<p class="text-sm mt-1" style="color: var(--text-secondary)">The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.</p>
|
||||
<p class="text-sm mt-2" style="color: var(--text-secondary)">The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.</p>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button type="submit" class="btn-primary px-6 py-2 rounded-lg font-medium">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@Icon("save", "h-4 w-4")
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">System Defaults</h3>
|
||||
<div class="mt-6 card p-6">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
@Icon("clock", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">System Defaults</h3>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Default Timezone</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Default Timezone</label>
|
||||
<select
|
||||
name="default_timezone"
|
||||
id="default_timezone"
|
||||
class="w-full px-4 py-2 rounded-lg border"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="UTC" selected?={ systemConfig["default_timezone"] == "UTC" }>UTC (UTC+0)</option>
|
||||
<option value="Pacific/Honolulu" selected?={ systemConfig["default_timezone"] == "Pacific/Honolulu" }>Hawaii (UTC-10)</option>
|
||||
@@ -86,16 +98,19 @@ templ AdminSettings(user User, systemConfig map[string]string, errorMessage stri
|
||||
<option value="Australia/Sydney" selected?={ systemConfig["default_timezone"] == "Australia/Sydney" }>Australian Eastern (UTC+10/+11)</option>
|
||||
<option value="Pacific/Auckland" selected?={ systemConfig["default_timezone"] == "Pacific/Auckland" }>New Zealand (UTC+12/+13)</option>
|
||||
</select>
|
||||
<p class="text-sm mt-1" style="color: var(--text-secondary)">Default timezone for users who haven't set their own.</p>
|
||||
<p class="text-sm mt-2" style="color: var(--text-secondary)">Default timezone for users who haven't set their own.</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-8 card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">URL Paths</h3>
|
||||
<div class="mt-6 card p-6">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
@Icon("external", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">URL Paths</h3>
|
||||
</div>
|
||||
<div class="space-y-2 text-sm" style="color: var(--text-secondary);">
|
||||
<p><strong>OPDS:</strong> { systemConfig["base_url"] }/opds</p>
|
||||
<p><strong>API:</strong> { systemConfig["base_url"] }/api</p>
|
||||
<p><strong>Device Sync:</strong> { systemConfig["base_url"] }/api/sync</p>
|
||||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">OPDS:</span> { systemConfig["base_url"] }/opds</p>
|
||||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">API:</span> { systemConfig["base_url"] }/api</p>
|
||||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Device Sync:</span> { systemConfig["base_url"] }/api/sync</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex min-h-screen\" style=\"background-color: var(--bg-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -45,322 +45,378 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center justify-between mb-4\"><a href=\"/admin\" class=\"btn-secondary px-4 py-2 rounded-lg font-medium\">← Back to Dashboard</a></div><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">System Settings</h1><p style=\"color: var(--text-secondary)\">Configure your Bookhoard instance</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center justify-between gap-4 flex-wrap mb-4\"><a href=\"/admin\" class=\"btn btn-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Back to Dashboard</a></div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("settings", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">System Settings</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Configure your Bookhoard instance</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if errorMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"mb-6 p-4 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--error); color: var(--error);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"mb-6 p-4 rounded-xl border flex items-start gap-3\" style=\"background-color: color-mix(in srgb, var(--status-danger) 12%, var(--bg-secondary)); border-color: var(--status-danger); color: var(--status-danger);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("alert", "h-5 w-5 shrink-0 mt-0.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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 29, Col: 22}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 36, Col: 28}
|
||||
}
|
||||
_, 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, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<form id=\"settings-form\" hx-put=\"/api/system/config\" hx-target=\"#settings-form\" hx-swap=\"outerHTML\"><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-xl font-semibold mb-6\" style=\"color: var(--text-primary)\">Base URL</h3><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-primary)\">Base URL</label> <input type=\"url\" name=\"base_url\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<form id=\"settings-form\" hx-put=\"/api/system/config\" hx-target=\"#settings-form\" hx-swap=\"outerHTML\"><div class=\"card p-6\"><div class=\"flex items-center gap-2 mb-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("globe", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Base URL</h3></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Base URL</label> <input type=\"url\" name=\"base_url\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 40, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 50, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" placeholder=\"https://books.example.com\" class=\"w-full px-4 py-2 rounded-lg border\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" required><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.</p></div><div class=\"mt-6 flex justify-end\"><button type=\"submit\" class=\"btn-primary px-6 py-2 rounded-lg font-medium\">Save Settings</button></div></div><div class=\"mt-8 card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-xl font-semibold mb-6\" style=\"color: var(--text-primary)\">System Defaults</h3><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-primary)\">Default Timezone</label> <select name=\"default_timezone\" id=\"default_timezone\" class=\"w-full px-4 py-2 rounded-lg border\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"UTC\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" placeholder=\"https://books.example.com\" class=\"input\" required><p class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.</p></div><div class=\"mt-6 flex justify-end\"><button type=\"submit\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Save Settings</button></div></div><div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("clock", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">System Defaults</h3></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Default Timezone</label> <select name=\"default_timezone\" id=\"default_timezone\" class=\"input\"><option value=\"UTC\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "UTC" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">UTC (UTC+0)</option> <option value=\"Pacific/Honolulu\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Pacific/Honolulu" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">Hawaii (UTC-10)</option> <option value=\"America/Anchorage\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Anchorage" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">Alaska (UTC-9/-8)</option> <option value=\"America/Los_Angeles\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Los_Angeles" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">Pacific (UTC-8/-7)</option> <option value=\"America/Denver\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">UTC (UTC+0)</option> <option value=\"Pacific/Honolulu\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Denver" {
|
||||
if systemConfig["default_timezone"] == "Pacific/Honolulu" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">Mountain (UTC-7/-6)</option> <option value=\"America/Phoenix\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">Hawaii (UTC-10)</option> <option value=\"America/Anchorage\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Phoenix" {
|
||||
if systemConfig["default_timezone"] == "America/Anchorage" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">Mountain - no DST (UTC-7)</option> <option value=\"America/Chicago\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">Alaska (UTC-9/-8)</option> <option value=\"America/Los_Angeles\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Chicago" {
|
||||
if systemConfig["default_timezone"] == "America/Los_Angeles" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, ">Central (UTC-6/-5)</option> <option value=\"America/New_York\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, ">Pacific (UTC-8/-7)</option> <option value=\"America/Denver\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/New_York" {
|
||||
if systemConfig["default_timezone"] == "America/Denver" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, ">Eastern (UTC-5/-4)</option> <option value=\"America/Sao_Paulo\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, ">Mountain (UTC-7/-6)</option> <option value=\"America/Phoenix\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Sao_Paulo" {
|
||||
if systemConfig["default_timezone"] == "America/Phoenix" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, ">Brasilia (UTC-3/-2)</option> <option value=\"Europe/London\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, ">Mountain - no DST (UTC-7)</option> <option value=\"America/Chicago\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Europe/London" {
|
||||
if systemConfig["default_timezone"] == "America/Chicago" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, ">British (UTC+0/+1)</option> <option value=\"Europe/Paris\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, ">Central (UTC-6/-5)</option> <option value=\"America/New_York\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Europe/Paris" {
|
||||
if systemConfig["default_timezone"] == "America/New_York" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">Central European (UTC+1/+2)</option> <option value=\"Europe/Helsinki\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">Eastern (UTC-5/-4)</option> <option value=\"America/Sao_Paulo\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Europe/Helsinki" {
|
||||
if systemConfig["default_timezone"] == "America/Sao_Paulo" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, ">Eastern European (UTC+2/+3)</option> <option value=\"Europe/Moscow\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, ">Brasilia (UTC-3/-2)</option> <option value=\"Europe/London\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Europe/Moscow" {
|
||||
if systemConfig["default_timezone"] == "Europe/London" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">Moscow (UTC+3)</option> <option value=\"Asia/Tehran\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">British (UTC+0/+1)</option> <option value=\"Europe/Paris\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Tehran" {
|
||||
if systemConfig["default_timezone"] == "Europe/Paris" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, ">Iran (UTC+3:30)</option> <option value=\"Asia/Dubai\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, ">Central European (UTC+1/+2)</option> <option value=\"Europe/Helsinki\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Dubai" {
|
||||
if systemConfig["default_timezone"] == "Europe/Helsinki" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, ">Gulf (UTC+4)</option> <option value=\"Asia/Karachi\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, ">Eastern European (UTC+2/+3)</option> <option value=\"Europe/Moscow\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Karachi" {
|
||||
if systemConfig["default_timezone"] == "Europe/Moscow" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, ">Pakistan (UTC+5)</option> <option value=\"Asia/Kolkata\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, ">Moscow (UTC+3)</option> <option value=\"Asia/Tehran\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Kolkata" {
|
||||
if systemConfig["default_timezone"] == "Asia/Tehran" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">India (UTC+5:30)</option> <option value=\"Asia/Dhaka\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">Iran (UTC+3:30)</option> <option value=\"Asia/Dubai\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Dhaka" {
|
||||
if systemConfig["default_timezone"] == "Asia/Dubai" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">Bangladesh (UTC+6)</option> <option value=\"Asia/Bangkok\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">Gulf (UTC+4)</option> <option value=\"Asia/Karachi\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Bangkok" {
|
||||
if systemConfig["default_timezone"] == "Asia/Karachi" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, ">Indochina (UTC+7)</option> <option value=\"Asia/Shanghai\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, ">Pakistan (UTC+5)</option> <option value=\"Asia/Kolkata\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Shanghai" {
|
||||
if systemConfig["default_timezone"] == "Asia/Kolkata" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">China (UTC+8)</option> <option value=\"Asia/Tokyo\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">India (UTC+5:30)</option> <option value=\"Asia/Dhaka\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Tokyo" {
|
||||
if systemConfig["default_timezone"] == "Asia/Dhaka" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, ">Japan/Korea (UTC+9)</option> <option value=\"Australia/Darwin\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, ">Bangladesh (UTC+6)</option> <option value=\"Asia/Bangkok\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Australia/Darwin" {
|
||||
if systemConfig["default_timezone"] == "Asia/Bangkok" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">Australian Central (UTC+9:30)</option> <option value=\"Australia/Sydney\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">Indochina (UTC+7)</option> <option value=\"Asia/Shanghai\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Australia/Sydney" {
|
||||
if systemConfig["default_timezone"] == "Asia/Shanghai" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, ">Australian Eastern (UTC+10/+11)</option> <option value=\"Pacific/Auckland\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, ">China (UTC+8)</option> <option value=\"Asia/Tokyo\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Pacific/Auckland" {
|
||||
if systemConfig["default_timezone"] == "Asia/Tokyo" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, ">New Zealand (UTC+12/+13)</option></select><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">Default timezone for users who haven't set their own.</p></div></div></form><div class=\"mt-8 card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-xl font-semibold mb-4\" style=\"color: var(--text-primary)\">URL Paths</h3><div class=\"space-y-2 text-sm\" style=\"color: var(--text-secondary);\"><p><strong>OPDS:</strong> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, ">Japan/Korea (UTC+9)</option> <option value=\"Australia/Darwin\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Australia/Darwin" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, ">Australian Central (UTC+9:30)</option> <option value=\"Australia/Sydney\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Australia/Sydney" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, ">Australian Eastern (UTC+10/+11)</option> <option value=\"Pacific/Auckland\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Pacific/Auckland" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, ">New Zealand (UTC+12/+13)</option></select><p class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">Default timezone for users who haven't set their own.</p></div></div></form><div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("external", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">URL Paths</h3></div><div class=\"space-y-2 text-sm\" style=\"color: var(--text-secondary);\"><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">OPDS:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 96, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 111, Col: 145}
|
||||
}
|
||||
_, 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, 56, "/opds</p><p><strong>API:</strong> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "/opds</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">API:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 97, Col: 59}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 112, Col: 144}
|
||||
}
|
||||
_, 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, 57, "/api</p><p><strong>Device Sync:</strong> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "/api</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Device Sync:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 98, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 113, Col: 152}
|
||||
}
|
||||
_, 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, 58, "/api/sync</p></div></div></div></main></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "/api/sync</p></div></div></div></main></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
package templates
|
||||
|
||||
templ AdminSidebar(user User, currentPath string) {
|
||||
<aside class="w-64 border-r" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<div class="p-6">
|
||||
<h2 class="text-lg font-semibold mb-6" style="color: var(--text-primary)">Admin Panel</h2>
|
||||
<nav class="space-y-2">
|
||||
<a href="/admin" class={activeClass(currentPath, "/admin")} style="color: var(--text-primary)">
|
||||
🏠 Dashboard
|
||||
<aside class="w-64 shrink-0 self-start sticky top-16 h-[calc(100vh-4rem)] overflow-y-auto border-r" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center gap-2 mb-6 px-2">
|
||||
<span class="grid place-items-center h-8 w-8 rounded-lg shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("shield", "h-4 w-4")
|
||||
</span>
|
||||
<h2 class="text-xs font-bold uppercase tracking-wide" style="color: var(--text-primary)">Admin Panel</h2>
|
||||
</div>
|
||||
<nav class="space-y-0.5">
|
||||
<a href="/admin" class={ activeClass(currentPath, "/admin") }>
|
||||
@Icon("grid", "h-5 w-5 shrink-0")
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="/admin/users" class={activeClass(currentPath, "/admin/users")} style="color: var(--text-primary)">
|
||||
👤 User Administration
|
||||
<a href="/admin/users" class={ activeClass(currentPath, "/admin/users") }>
|
||||
@Icon("users", "h-5 w-5 shrink-0")
|
||||
<span>Users</span>
|
||||
</a>
|
||||
<a href="/admin/library" class={activeClass(currentPath, "/admin/library")} style="color: var(--text-primary)">
|
||||
📚 Library Management
|
||||
<a href="/admin/library" class={ activeClass(currentPath, "/admin/library") }>
|
||||
@Icon("library", "h-5 w-5 shrink-0")
|
||||
<span>Library</span>
|
||||
</a>
|
||||
<a href="/admin/settings" class={activeClass(currentPath, "/admin/settings")} style="color: var(--text-primary)">
|
||||
⚙️ System Settings
|
||||
<a href="/admin/settings" class={ activeClass(currentPath, "/admin/settings") }>
|
||||
@Icon("settings", "h-5 w-5 shrink-0")
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,15 @@ func AdminSidebar(user User, currentPath string) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<aside class=\"w-64 border-r\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><div class=\"p-6\"><h2 class=\"text-lg font-semibold mb-6\" style=\"color: var(--text-primary)\">Admin Panel</h2><nav class=\"space-y-2\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<aside class=\"w-64 shrink-0 self-start sticky top-16 h-[calc(100vh-4rem)] overflow-y-auto border-r\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"p-5\"><div class=\"flex items-center gap-2 mb-6 px-2\"><span class=\"grid place-items-center h-8 w-8 rounded-lg shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("shield", "h-4 w-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><h2 class=\"text-xs font-bold uppercase tracking-wide\" style=\"color: var(--text-primary)\">Admin Panel</h2></div><nav class=\"space-y-0.5\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -38,7 +46,7 @@ func AdminSidebar(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"/admin\" class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<a href=\"/admin\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -51,7 +59,15 @@ func AdminSidebar(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" style=\"color: var(--text-primary)\">🏠 Dashboard</a> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("grid", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span>Dashboard</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -60,7 +76,7 @@ func AdminSidebar(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<a href=\"/admin/users\" class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"/admin/users\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -73,7 +89,15 @@ func AdminSidebar(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" style=\"color: var(--text-primary)\">👤 User Administration</a> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("users", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span>Users</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -82,7 +106,7 @@ func AdminSidebar(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"/admin/library\" class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"/admin/library\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -95,7 +119,15 @@ func AdminSidebar(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" style=\"color: var(--text-primary)\">📚 Library Management</a> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("library", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span>Library</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -104,7 +136,7 @@ func AdminSidebar(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"/admin/settings\" class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a href=\"/admin/settings\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -117,7 +149,15 @@ func AdminSidebar(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" style=\"color: var(--text-primary)\">⚙️ System Settings</a></nav></div></aside>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("settings", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span>Settings</span></a></nav></div></aside>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func Admin(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex min-h-screen\" style=\"background-color: var(--bg-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -45,7 +45,79 @@ func Admin(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">Dashboard</h1><p style=\"color: var(--text-secondary)\">Overview of your Bookhoard library and settings</p></div><div class=\"grid grid-cols-1 md:grid-cols-2 gap-6 mb-8\"><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><div class=\"flex items-center space-x-3\"><div class=\"text-3xl\">📖</div><div><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">Library</h3><p style=\"color: var(--text-secondary)\" class=\"text-sm\">Manage your ebook collection</p></div></div><a href=\"/\" class=\"mt-4 inline-block text-sm btn-secondary px-3 py-1 rounded\">View Library</a></div><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><div class=\"flex items-center space-x-3\"><div class=\"text-3xl\">👁️</div><div><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">Scan Watch Status</h3><p style=\"color: var(--text-secondary)\" class=\"text-sm\">Auto-detecting new files</p></div></div><div id=\"watch-status\" class=\"mt-4 text-sm\" style=\"color: var(--text-secondary)\"><span class=\"inline-block w-2 h-2 rounded-full bg-green-500 mr-2\"></span> Watching <span id=\"watch-count\">0</span> libraries</div></div></div><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h3 class=\"text-xl font-semibold mb-4\" style=\"color: var(--text-primary)\">Quick Actions</h3><div class=\"grid grid-cols-1 sm:grid-cols-2 gap-4\"><button @click=\"scanAllLibraries()\" class=\"btn-primary p-4 rounded-lg text-left\"><div class=\"font-medium\">Rescan Library</div><div style=\"color: var(--text-secondary)\" class=\"text-sm\">Re-scan existing files and fix metadata</div></button> <a href=\"/admin/library\" class=\"btn-secondary p-4 rounded-lg text-left block\"><div class=\"font-medium\">Manage Libraries and Folders</div><div style=\"color: var(--text-secondary)\" class=\"text-sm\">Add or remove libraries and scan directories</div></a></div></div><!-- Scan Progress Section --><div id=\"scan-progress-container\" class=\"hidden mt-6 p-6 rounded-lg border opacity-0 -translate-y-2.5 transition-all duration-300 ease-out\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-4\"><h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">📚 Scanning Libraries</h3><button @click=\"hideScanProgress()\" class=\"p-2 hover:bg-gray-700 rounded\">✕</button></div><!-- Overall Progress --><div class=\"mb-4\"><div class=\"flex justify-between text-sm mb-2\"><span style=\"color: var(--text-secondary)\">Overall Progress</span> <span id=\"scan-progress-text\" style=\"color: var(--text-primary)\">0%</span></div><div class=\"w-full bg-gray-700 rounded-full h-3\"><div id=\"scan-progress-bar\" class=\"h-3 rounded-full transition-all duration-500\" style=\"width: 0%; background-color: var(--accent);\"></div></div><div id=\"scan-status\" class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">Starting scan...</div></div><!-- Per-Library Progress --><div id=\"library-progress-list\" class=\"space-y-3\"><!-- Dynamically populated --></div><!-- Results Summary --><div id=\"scan-results\" class=\"hidden mt-6 p-4 rounded-lg border\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><h4 class=\"font-semibold mb-2\" style=\"color: var(--text-primary)\">✅ Scan Complete!</h4><div id=\"scan-results-content\" style=\"color: var(--text-secondary)\"><!-- Results populated by JS --></div><div class=\"mt-4 flex gap-2\"><button @click=\"window.location.reload()\" class=\"btn-primary px-4 py-2 rounded-lg\">Refresh to View Books</button> <button @click=\"hideScanProgress()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Dismiss</button></div></div></div></div></main></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("grid", "h-5 w-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><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Dashboard</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Overview of your Bookhoard library and settings</p></div><div class=\"grid grid-cols-1 md:grid-cols-2 gap-6 mb-6\"><div class=\"stat-card\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-11 w-11 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("library", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span><div><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">Library</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Manage your ebook collection</p></div></div><a href=\"/\" class=\"btn btn-secondary mt-4 text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("arrow-right", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "View Library</a></div><div class=\"stat-card\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-11 w-11 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("sync", "h-5 w-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><div><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">Scan Watch Status</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Auto-detecting new files</p></div></div><div id=\"watch-status\" class=\"mt-4 text-sm\" style=\"color: var(--text-secondary)\"><span class=\"inline-block w-2 h-2 rounded-full mr-2\" style=\"background-color: var(--status-success);\"></span> Watching <span id=\"watch-count\">0</span> libraries</div></div></div><div class=\"card p-6\"><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary)\">Quick Actions</h3><div class=\"grid grid-cols-1 sm:grid-cols-2 gap-4\"><button @click=\"scanAllLibraries()\" class=\"btn btn-primary py-4 flex-col items-start gap-1\"><span class=\"flex items-center gap-2 font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("refresh", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "Rescan Library</span> <span class=\"text-xs font-normal opacity-80\">Re-scan existing files and fix metadata</span></button> <a href=\"/admin/library\" class=\"btn btn-secondary py-4 flex-col items-start gap-1\"><span class=\"flex items-center gap-2 font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("library", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Manage Libraries</span> <span class=\"text-xs font-normal opacity-80\">Add or remove libraries and scan directories</span></a></div></div><!-- Scan Progress Section --><div id=\"scan-progress-container\" class=\"card hidden mt-6 p-6 opacity-0 -translate-y-2.5 transition-all duration-300 ease-out\"><div class=\"flex justify-between items-center mb-4\"><h3 class=\"text-lg font-semibold flex items-center gap-2\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("refresh", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "Scanning Libraries</h3><button @click=\"hideScanProgress()\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</button></div><!-- Overall Progress --><div class=\"mb-4\"><div class=\"flex justify-between text-sm mb-2\"><span style=\"color: var(--text-secondary)\">Overall Progress</span> <span id=\"scan-progress-text\" style=\"color: var(--text-primary)\">0%</span></div><div class=\"w-full rounded-full h-3\" style=\"background-color: var(--surface-hover);\"><div id=\"scan-progress-bar\" class=\"h-3 rounded-full transition-all duration-500\" style=\"width: 0%; background-color: var(--accent);\"></div></div><div id=\"scan-status\" class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">Starting scan...</div></div><!-- Per-Library Progress --><div id=\"library-progress-list\" class=\"space-y-3\"><!-- Dynamically populated --></div><!-- Results Summary --><div id=\"scan-results\" class=\"hidden mt-6 p-4 rounded-xl border\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><h4 class=\"font-semibold mb-2 flex items-center gap-2\" style=\"color: var(--status-success);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("check-circle", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Scan Complete!</h4><div id=\"scan-results-content\" style=\"color: var(--text-secondary)\"><!-- Results populated by JS --></div><div class=\"mt-4 flex gap-2\"><button @click=\"window.location.reload()\" class=\"btn btn-primary\">Refresh to View Books</button> <button @click=\"hideScanProgress()\" class=\"btn btn-secondary\">Dismiss</button></div></div></div></div></main></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+119
-109
@@ -13,119 +13,129 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
|
||||
@Header(currentUser, "/admin/users")
|
||||
<!-- Modal Container (populated by HTMX) -->
|
||||
<div id="modal-container"></div>
|
||||
<div class="flex min-h-screen" style="background-color: var(--bg-primary)">
|
||||
<div class="flex">
|
||||
@AdminSidebar(currentUser, "/admin/users")
|
||||
<main class="flex-1 p-8">
|
||||
<div class="w-full">
|
||||
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">User Management</h1>
|
||||
<p style="color: var(--text-secondary)">Manage user accounts and permissions</p>
|
||||
</div>
|
||||
<!-- Users Table -->
|
||||
<div class="card rounded-lg border overflow-hidden" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<table class="w-full">
|
||||
<thead style="background-color: var(--bg-primary)">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Username</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Email</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Role</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Created</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y" style="divide-color: var(--border)">
|
||||
for _, user := range users {
|
||||
<tr id={ "user-" + user.ID } class="hover:bg-opacity-50" style="transition: background-color 0.2s;">
|
||||
<!-- Username -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<div class="text-sm font-medium" style="color: var(--text-primary)">{ user.Username }</div>
|
||||
if user.ID == currentUser.ID {
|
||||
<span class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: white;">You</span>
|
||||
<div class="max-w-5xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("users", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">User Management</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Manage user accounts and permissions</p>
|
||||
</div>
|
||||
<!-- Users Table -->
|
||||
<div class="card overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead style="background-color: var(--bg-primary)">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Username</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Email</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Role</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Created</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y" style="divide-color: var(--border)">
|
||||
for _, user := range users {
|
||||
<tr id={ "user-" + user.ID } class="transition-colors hover:bg-surface-hover">
|
||||
<!-- Username -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="grid place-items-center h-8 w-8 rounded-full shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("user", "h-4 w-4")
|
||||
</span>
|
||||
<div>
|
||||
<div class="text-sm font-medium" style="color: var(--text-primary)">{ user.Username }</div>
|
||||
if user.ID == currentUser.ID {
|
||||
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">You</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Email -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm" style="color: var(--text-primary)">{ user.Email }</div>
|
||||
</td>
|
||||
<!-- Role Toggle (with last-admin protection) -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
if user.Role == "admin" && adminCount == 1 {
|
||||
<!-- Last admin - disabled -->
|
||||
<div class="relative">
|
||||
<select
|
||||
disabled
|
||||
class="input w-auto py-1 pr-7 text-xs opacity-50 cursor-not-allowed"
|
||||
title="Cannot demote the last admin"
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="admin" selected>Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
} else {
|
||||
<select
|
||||
hx-put={ "/api/auth/profile/" + user.ID }
|
||||
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
||||
hx-target={ "#role-result-" + user.ID }
|
||||
hx-swap="innerHTML"
|
||||
name="role"
|
||||
class="input w-auto py-1 pr-7 text-xs"
|
||||
onchange="this.dispatchEvent(new Event('htmx:trigger'))"
|
||||
hx-trigger="change"
|
||||
hx-vals='{"role": this.value}'
|
||||
>
|
||||
<option value="user" selected?={ user.Role == "user" }>User</option>
|
||||
<option value="admin" selected?={ user.Role == "admin" }>Admin</option>
|
||||
</select>
|
||||
<div id={ "role-result-" + user.ID } class="text-xs mt-1"></div>
|
||||
}
|
||||
</td>
|
||||
<!-- Created -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm" style="color: var(--text-secondary)">{ FormatInTimezone(user.CreatedAt, currentUser.Timezone) }</div>
|
||||
</td>
|
||||
<!-- Actions -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<button
|
||||
hx-get={ "/admin/users/" + user.ID + "/profile-modal" }
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML"
|
||||
class="btn btn-secondary text-xs px-2.5 py-1"
|
||||
>
|
||||
@Icon("edit", "h-4 w-4")
|
||||
Edit
|
||||
</button>
|
||||
if user.Role == "admin" && adminCount == 1 {
|
||||
<button
|
||||
disabled
|
||||
class="btn btn-danger text-xs px-2.5 py-1"
|
||||
title="Cannot delete the last admin"
|
||||
>
|
||||
@Icon("trash", "h-4 w-4")
|
||||
Delete
|
||||
</button>
|
||||
} else {
|
||||
<button
|
||||
hx-delete={ "/api/auth/profile/" + user.ID }
|
||||
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
||||
hx-target={ "#user-" + user.ID }
|
||||
hx-swap="outerHTML swap:0.5s"
|
||||
hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
|
||||
class="btn btn-danger text-xs px-2.5 py-1"
|
||||
>
|
||||
@Icon("trash", "h-4 w-4")
|
||||
Delete
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Email -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm" style="color: var(--text-primary)">{ user.Email }</div>
|
||||
</td>
|
||||
<!-- Role Toggle (with last-admin protection) -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
if user.Role == "admin" && adminCount == 1 {
|
||||
<!-- Last admin - disabled -->
|
||||
<div class="relative">
|
||||
<select
|
||||
disabled
|
||||
class="text-sm rounded px-2 py-1 cursor-not-allowed opacity-50"
|
||||
style="background-color: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border);"
|
||||
title="Cannot demote the last admin"
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="admin" selected>Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
} else {
|
||||
<select
|
||||
hx-put={ "/api/auth/profile/" + user.ID }
|
||||
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
||||
hx-target={ "#role-result-" + user.ID }
|
||||
hx-swap="innerHTML"
|
||||
name="role"
|
||||
class="text-sm rounded px-2 py-1"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
|
||||
onchange="this.dispatchEvent(new Event('htmx:trigger'))"
|
||||
hx-trigger="change"
|
||||
hx-vals='{"role": this.value}'
|
||||
>
|
||||
<option value="user" selected?={ user.Role == "user" }>User</option>
|
||||
<option value="admin" selected?={ user.Role == "admin" }>Admin</option>
|
||||
</select>
|
||||
<div id={ "role-result-" + user.ID } class="text-xs mt-1"></div>
|
||||
}
|
||||
</td>
|
||||
<!-- Created -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm" style="color: var(--text-secondary)">{ FormatInTimezone(user.CreatedAt, currentUser.Timezone) }</div>
|
||||
</td>
|
||||
<!-- Actions -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<button
|
||||
hx-get={ "/admin/users/" + user.ID + "/profile-modal" }
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML"
|
||||
class="text-sm px-3 py-1 rounded mr-2"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
if user.Role == "admin" && adminCount == 1 {
|
||||
<button
|
||||
disabled
|
||||
class="text-sm px-3 py-1 rounded cursor-not-allowed opacity-50"
|
||||
style="background-color: #dc2626; color: white;"
|
||||
title="Cannot delete the last admin"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
} else {
|
||||
<button
|
||||
hx-delete={ "/api/auth/profile/" + user.ID }
|
||||
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
||||
hx-target={ "#user-" + user.ID }
|
||||
hx-swap="outerHTML swap:0.5s"
|
||||
hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
|
||||
class="text-sm px-3 py-1 rounded"
|
||||
style="background-color: #dc2626; color: white;"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Modal Container (populated by HTMX) --><div id=\"modal-container\"></div><div class=\"flex min-h-screen\" style=\"background-color: var(--bg-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Modal Container (populated by HTMX) --><div id=\"modal-container\"></div><div class=\"flex\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -45,207 +45,247 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"w-full\"><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">User Management</h1><p style=\"color: var(--text-secondary)\">Manage user accounts and permissions</p></div><!-- Users Table --><div class=\"card rounded-lg border overflow-hidden\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><table class=\"w-full\"><thead style=\"background-color: var(--bg-primary)\"><tr><th class=\"px-6 py-3 text-left text-xs font-medium uppercase tracking-wider\" style=\"color: var(--text-secondary)\">Username</th><th class=\"px-6 py-3 text-left text-xs font-medium uppercase tracking-wider\" style=\"color: var(--text-secondary)\">Email</th><th class=\"px-6 py-3 text-left text-xs font-medium uppercase tracking-wider\" style=\"color: var(--text-secondary)\">Role</th><th class=\"px-6 py-3 text-left text-xs font-medium uppercase tracking-wider\" style=\"color: var(--text-secondary)\">Created</th><th class=\"px-6 py-3 text-right text-xs font-medium uppercase tracking-wider\" style=\"color: var(--text-secondary)\">Actions</th></tr></thead> <tbody class=\"divide-y\" style=\"divide-color: var(--border)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-5xl\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("users", "h-5 w-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><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">User Management</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Manage user accounts and permissions</p></div><!-- Users Table --><div class=\"card overflow-hidden\"><table class=\"w-full\"><thead style=\"background-color: var(--bg-primary)\"><tr><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Username</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Email</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Role</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Created</th><th class=\"px-6 py-3 text-right text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Actions</th></tr></thead> <tbody class=\"divide-y\" style=\"divide-color: var(--border)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, user := range users {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<tr id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<tr id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("user-" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 37, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 43, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"hover:bg-opacity-50\" style=\"transition: background-color 0.2s;\"><!-- Username --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"flex items-center\"><div><div class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"transition-colors hover:bg-surface-hover\"><!-- Username --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"flex items-center gap-2\"><span class=\"grid place-items-center h-8 w-8 rounded-full shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("user", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span><div><div class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 42, Col: 96}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 51, Col: 97}
|
||||
}
|
||||
_, 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, 6, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.ID == currentUser.ID {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"text-xs px-2 py-1 rounded\" style=\"background-color: var(--accent); color: white;\">You</span>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">You</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div></td><!-- Email --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></div></td><!-- Email --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 51, Col: 79}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 60, Col: 80}
|
||||
}
|
||||
_, 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, 9, "</div></td><!-- Role Toggle (with last-admin protection) --><td class=\"px-6 py-4 whitespace-nowrap\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></td><!-- Role Toggle (with last-admin protection) --><td class=\"px-6 py-4 whitespace-nowrap\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "admin" && adminCount == 1 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<!-- Last admin - disabled --> <div class=\"relative\"><select disabled class=\"text-sm rounded px-2 py-1 cursor-not-allowed opacity-50\" style=\"background-color: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border);\" title=\"Cannot demote the last admin\"><option value=\"user\">User</option> <option value=\"admin\" selected>Admin</option></select></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<!-- Last admin - disabled --> <div class=\"relative\"><select disabled class=\"input w-auto py-1 pr-7 text-xs opacity-50 cursor-not-allowed\" title=\"Cannot demote the last admin\"><option value=\"user\">User</option> <option value=\"admin\" selected>Admin</option></select></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<select hx-put=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<select hx-put=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/profile/" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 70, Col: 52}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 78, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("#role-result-" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 72, Col: 50}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 80, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" hx-swap=\"innerHTML\" name=\"role\" class=\"text-sm rounded px-2 py-1\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);\" onchange=\"this.dispatchEvent(new Event('htmx:trigger'))\" hx-trigger=\"change\" hx-vals='{\"role\": this.value}'><option value=\"user\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" hx-swap=\"innerHTML\" name=\"role\" class=\"input w-auto py-1 pr-7 text-xs\" onchange=\"this.dispatchEvent(new Event('htmx:trigger'))\" hx-trigger=\"change\" hx-vals='{\"role\": this.value}'><option value=\"user\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "user" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">User</option> <option value=\"admin\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "admin" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">Admin</option></select><div id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">User</option> <option value=\"admin\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "admin" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">Admin</option></select><div id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("role-result-" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 84, Col: 46}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 91, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" class=\"text-xs mt-1\"></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"text-xs mt-1\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><!-- Created --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</td><!-- Created --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(user.CreatedAt, currentUser.Timezone))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 89, Col: 125}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 96, Col: 126}
|
||||
}
|
||||
_, 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, 20, "</div></td><!-- Actions --><td class=\"px-6 py-4 whitespace-nowrap text-right\"><button hx-get=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></td><!-- Actions --><td class=\"px-6 py-4 whitespace-nowrap text-right\"><div class=\"inline-flex items-center gap-1\"><button hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/users/" + user.ID + "/profile-modal")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 94, Col: 65}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 102, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"text-sm px-3 py-1 rounded mr-2\" style=\"background-color: var(--accent); color: white;\">Edit</button> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn btn-secondary text-xs px-2.5 py-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("edit", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Edit</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "admin" && adminCount == 1 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<button disabled class=\"text-sm px-3 py-1 rounded cursor-not-allowed opacity-50\" style=\"background-color: #dc2626; color: white;\" title=\"Cannot delete the last admin\">Delete</button>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<button disabled class=\"btn btn-danger text-xs px-2.5 py-1\" title=\"Cannot delete the last admin\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Delete</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<button hx-delete=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<button hx-delete=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/profile/" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 113, Col: 55}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 121, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue("#user-" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 115, Col: 43}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 123, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" hx-swap=\"outerHTML swap:0.5s\" hx-confirm=\"Are you sure you want to delete this user? This action cannot be undone.\" class=\"text-sm px-3 py-1 rounded\" style=\"background-color: #dc2626; color: white;\">Delete</button>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" hx-swap=\"outerHTML swap:0.5s\" hx-confirm=\"Are you sure you want to delete this user? This action cannot be undone.\" class=\"btn btn-danger text-xs px-2.5 py-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Delete</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td></tr>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</tbody></table></div></main></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</tbody></table></div></div></main></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+35
-38
@@ -12,78 +12,75 @@ templ Analytics(user User) {
|
||||
</head>
|
||||
<body x-data="analytics" x-init="loadAnalytics" class="theme-{ user.Theme }">
|
||||
@Header(user, "/analytics")
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">📊 Reading Analytics</h1>
|
||||
<p style="color: var(--text-secondary)">Track your reading habits and device usage</p>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("chart", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Reading Analytics</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Track your reading habits and device usage</p>
|
||||
</div>
|
||||
<!-- Date Range Picker -->
|
||||
<div class="card p-4 rounded-lg border mb-6" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="flex flex-wrap gap-4 items-center">
|
||||
<div class="card p-4 mb-6">
|
||||
<div class="flex flex-wrap gap-4 items-end">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Start Date</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Start Date</label>
|
||||
<input
|
||||
type="date"
|
||||
id="start-date"
|
||||
onchange="loadAnalytics()"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">End Date</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">End Date</label>
|
||||
<input
|
||||
type="date"
|
||||
id="end-date"
|
||||
onchange="loadAnalytics()"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button @click="loadAnalytics()" class="btn-primary px-6 py-2 rounded-lg">Update</button>
|
||||
<div>
|
||||
<button @click="loadAnalytics()" class="btn btn-primary">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8" id="stats-container">
|
||||
<div class="card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-secondary);">Books Read</h3>
|
||||
<p id="total-books" class="text-3xl font-bold" style="color: var(--text-primary);">-</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8" id="stats-container">
|
||||
<div class="stat-card">
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Books Read</h3>
|
||||
<p id="total-books" class="text-3xl font-bold" style="color: var(--accent);">-</p>
|
||||
</div>
|
||||
<div class="card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-secondary);">Pages Read</h3>
|
||||
<p id="total-pages" class="text-3xl font-bold" style="color: var(--text-primary);">-</p>
|
||||
<div class="stat-card">
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Pages Read</h3>
|
||||
<p id="total-pages" class="text-3xl font-bold" style="color: var(--accent);">-</p>
|
||||
</div>
|
||||
<div class="card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-secondary);">Reading Time</h3>
|
||||
<p id="reading-time" class="text-3xl font-bold" style="color: var(--text-primary);">-</p>
|
||||
<div class="stat-card">
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Reading Time</h3>
|
||||
<p id="reading-time" class="text-3xl font-bold" style="color: var(--accent);">-</p>
|
||||
</div>
|
||||
<div class="card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-secondary);">Completion Rate</h3>
|
||||
<p id="completion-rate" class="text-3xl font-bold" style="color: var(--text-primary);">-</p>
|
||||
<div class="stat-card">
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Completion Rate</h3>
|
||||
<p id="completion-rate" class="text-3xl font-bold" style="color: var(--accent);">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Charts Row -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<!-- Daily Reading Chart -->
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary);">Daily Reading Minutes</h3>
|
||||
<div class="card p-6">
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide mb-4" style="color: var(--text-secondary);">Daily Reading Minutes</h3>
|
||||
<div class="h-80">
|
||||
<canvas id="daily-reading-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Usage Chart -->
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary);">Device Usage</h3>
|
||||
<div class="card p-6">
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide mb-4" style="color: var(--text-secondary);">Device Usage</h3>
|
||||
<div class="h-80">
|
||||
<canvas id="device-usage-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Popular Books -->
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary);">Most Read Books</h3>
|
||||
<div class="card p-6">
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide mb-4" style="color: var(--text-secondary);">Most Read Books</h3>
|
||||
<div id="popular-books" class="space-y-3">
|
||||
<div class="text-center py-8" style="color: var(--text-secondary);">
|
||||
<div class="loading-spinner mx-auto mb-4"></div>
|
||||
|
||||
@@ -37,7 +37,15 @@ func Analytics(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">📊 Reading Analytics</h1><p style=\"color: var(--text-secondary)\">Track your reading habits and device usage</p></div><!-- Date Range Picker --><div class=\"card p-4 rounded-lg border mb-6\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex flex-wrap gap-4 items-center\"><div class=\"flex-1 min-w-[200px]\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary);\">Start Date</label> <input type=\"date\" id=\"start-date\" onchange=\"loadAnalytics()\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div class=\"flex-1 min-w-[200px]\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary);\">End Date</label> <input type=\"date\" id=\"end-date\" onchange=\"loadAnalytics()\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div class=\"flex items-end\"><button @click=\"loadAnalytics()\" class=\"btn-primary px-6 py-2 rounded-lg\">Update</button></div></div></div><!-- Stats Cards --><div class=\"grid grid-cols-1 md:grid-cols-4 gap-6 mb-8\" id=\"stats-container\"><div class=\"card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-secondary);\">Books Read</h3><p id=\"total-books\" class=\"text-3xl font-bold\" style=\"color: var(--text-primary);\">-</p></div><div class=\"card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-secondary);\">Pages Read</h3><p id=\"total-pages\" class=\"text-3xl font-bold\" style=\"color: var(--text-primary);\">-</p></div><div class=\"card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-secondary);\">Reading Time</h3><p id=\"reading-time\" class=\"text-3xl font-bold\" style=\"color: var(--text-primary);\">-</p></div><div class=\"card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-secondary);\">Completion Rate</h3><p id=\"completion-rate\" class=\"text-3xl font-bold\" style=\"color: var(--text-primary);\">-</p></div></div><!-- Charts Row --><div class=\"grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8\"><!-- Daily Reading Chart --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary);\">Daily Reading Minutes</h3><div class=\"h-80\"><canvas id=\"daily-reading-chart\"></canvas></div></div><!-- Device Usage Chart --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary);\">Device Usage</h3><div class=\"h-80\"><canvas id=\"device-usage-chart\"></canvas></div></div></div><!-- Popular Books --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary);\">Most Read Books</h3><div id=\"popular-books\" class=\"space-y-3\"><div class=\"text-center py-8\" style=\"color: var(--text-secondary);\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading analytics...</p></div></div></div></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("chart", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Reading Analytics</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Track your reading habits and device usage</p></div><div class=\"card p-4 mb-6\"><div class=\"flex flex-wrap gap-4 items-end\"><div class=\"flex-1 min-w-[200px]\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">Start Date</label> <input type=\"date\" id=\"start-date\" onchange=\"loadAnalytics()\" class=\"input\"></div><div class=\"flex-1 min-w-[200px]\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">End Date</label> <input type=\"date\" id=\"end-date\" onchange=\"loadAnalytics()\" class=\"input\"></div><div><button @click=\"loadAnalytics()\" class=\"btn btn-primary\">Update</button></div></div></div><div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8\" id=\"stats-container\"><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">Books Read</h3><p id=\"total-books\" class=\"text-3xl font-bold\" style=\"color: var(--accent);\">-</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">Pages Read</h3><p id=\"total-pages\" class=\"text-3xl font-bold\" style=\"color: var(--accent);\">-</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">Reading Time</h3><p id=\"reading-time\" class=\"text-3xl font-bold\" style=\"color: var(--accent);\">-</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">Completion Rate</h3><p id=\"completion-rate\" class=\"text-3xl font-bold\" style=\"color: var(--accent);\">-</p></div></div><div class=\"grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8\"><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary);\">Daily Reading Minutes</h3><div class=\"h-80\"><canvas id=\"daily-reading-chart\"></canvas></div></div><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary);\">Device Usage</h3><div class=\"h-80\"><canvas id=\"device-usage-chart\"></canvas></div></div></div><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary);\">Most Read Books</h3><div id=\"popular-books\" class=\"space-y-3\"><div class=\"text-center py-8\" style=\"color: var(--text-secondary);\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading analytics...</p></div></div></div></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -2,64 +2,97 @@ package templates
|
||||
|
||||
templ APIExplorer(explorer APIExplorerData) {
|
||||
if !explorer.IsLoggedIn {
|
||||
// Show mode toggle and mock data
|
||||
<div x-data="devices" class="border border-border rounded-lg p-6 mt-8 bg-background-secondary">
|
||||
<div class="flex gap-2 mb-4">
|
||||
<button class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary cursor-pointer bg-accent text-white">Mock Data</button>
|
||||
<button class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary opacity-50 cursor-not-allowed" disabled>Login to Try Real</button>
|
||||
<div x-data="devices" class="card p-6 mt-8 space-y-5">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
if explorer.Endpoint.Method == "GET" {
|
||||
<span class="badge status-completed">{ explorer.Endpoint.Method }</span>
|
||||
} else if explorer.Endpoint.Method == "DELETE" {
|
||||
<span class="badge status-failed">{ explorer.Endpoint.Method }</span>
|
||||
} else if explorer.Endpoint.Method == "POST" {
|
||||
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">{ explorer.Endpoint.Method }</span>
|
||||
} else {
|
||||
<span class="badge status-processing">{ explorer.Endpoint.Method }</span>
|
||||
}
|
||||
<code class="text-sm" style="color: var(--text-secondary);">{ explorer.Endpoint.Path }</code>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-4">
|
||||
<select disabled class="w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent">
|
||||
<option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option>
|
||||
<option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option>
|
||||
<option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option>
|
||||
<option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option>
|
||||
</select>
|
||||
</div>
|
||||
<h4 class="text-text-secondary mb-2 text-sm font-medium">Request Body (Example)</h4>
|
||||
<pre class="bg-background-primary p-4 rounded-lg overflow-x-auto"><code class="text-text-primary text-sm">{ explorer.Endpoint.RequestBody }</code></pre>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-primary">Mock Data</button>
|
||||
<button class="btn btn-secondary opacity-50 cursor-not-allowed" disabled>Login to Try Real</button>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h4 class="text-text-secondary mb-2 text-sm font-medium">Response (Mock)</h4>
|
||||
<pre class="bg-background-primary p-4 rounded-lg overflow-x-auto"><code class="text-text-primary text-sm">{ explorer.Endpoint.Response }</code></pre>
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Request Body (Example)</p>
|
||||
<pre class="card p-4 overflow-x-auto"><code class="text-sm" style="color: var(--text-primary);">{ explorer.Endpoint.RequestBody }</code></pre>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button @click="copyToClipboard(JSON.stringify({ explorer.Endpoint.RequestBody }, null, 2))" class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm">Copy Request</button>
|
||||
<button @click="copyToClipboard(JSON.stringify({ explorer.Endpoint.Response }, null, 2))" class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm">Copy Response</button>
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Response (Mock)</p>
|
||||
<pre class="card p-4 overflow-x-auto"><code class="text-sm" style="color: var(--text-primary);">{ explorer.Endpoint.Response }</code></pre>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button @click="copyToClipboard(JSON.stringify({ explorer.Endpoint.RequestBody }, null, 2))" class="btn btn-secondary text-sm">
|
||||
@Icon("copy", "h-4 w-4")
|
||||
Copy Request
|
||||
</button>
|
||||
<button @click="copyToClipboard(JSON.stringify({ explorer.Endpoint.Response }, null, 2))" class="btn btn-secondary text-sm">
|
||||
@Icon("copy", "h-4 w-4")
|
||||
Copy Response
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
// Show interactive explorer with real execution
|
||||
<div class="border border-border rounded-lg p-6 mt-8 bg-background-secondary" x-data="apiExplorerDoc" x-init="initAPIExplorerDoc('{ explorer.Endpoint.Path }', '{ explorer.Endpoint.RequestBody }', '{ explorer.Endpoint.Response }')">
|
||||
<div class="flex gap-2 mb-4">
|
||||
<button class="mode-btn px-4 py-2 border border-border rounded bg-background-primary text-text-primary cursor-pointer hover:bg-background-secondary text-sm" id="mock-btn" @click="showDocMode('mock')">Mock Data</button>
|
||||
<button class="mode-btn px-4 py-2 bg-accent text-white rounded cursor-pointer text-sm" id="real-btn" @click="showDocMode('real')">Try It Out</button>
|
||||
<div class="card p-6 mt-8 space-y-5" x-data="apiExplorerDoc" x-init="initAPIExplorerDoc('{ explorer.Endpoint.Path }', '{ explorer.Endpoint.RequestBody }', '{ explorer.Endpoint.Response }')">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
if explorer.Endpoint.Method == "GET" {
|
||||
<span class="badge status-completed">{ explorer.Endpoint.Method }</span>
|
||||
} else if explorer.Endpoint.Method == "DELETE" {
|
||||
<span class="badge status-failed">{ explorer.Endpoint.Method }</span>
|
||||
} else if explorer.Endpoint.Method == "POST" {
|
||||
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">{ explorer.Endpoint.Method }</span>
|
||||
} else {
|
||||
<span class="badge status-processing">{ explorer.Endpoint.Method }</span>
|
||||
}
|
||||
<code class="text-sm" style="color: var(--text-secondary);">{ explorer.Endpoint.Path }</code>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-4">
|
||||
<select id="http-method" class="w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent">
|
||||
<option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option>
|
||||
<option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option>
|
||||
<option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option>
|
||||
<option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option>
|
||||
</select>
|
||||
<div class="flex gap-2">
|
||||
<button class="mode-btn btn btn-secondary text-sm" id="mock-btn" @click="showDocMode('mock')">Mock Data</button>
|
||||
<button class="mode-btn btn btn-primary text-sm" id="real-btn" @click="showDocMode('real')">Try It Out</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="http-method" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Method</label>
|
||||
<select id="http-method" class="input">
|
||||
<option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option>
|
||||
<option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option>
|
||||
<option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option>
|
||||
<option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="request-body" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Request Body</label>
|
||||
<textarea id="request-body" placeholder="Edit request body…" class="input min-h-[150px] font-mono">{ explorer.Endpoint.RequestBody }</textarea>
|
||||
</div>
|
||||
<button id="try-it-out" @click="tryDocEndpoint()" class="btn btn-primary">
|
||||
@Icon("play", "h-4 w-4")
|
||||
Try It Out
|
||||
</button>
|
||||
<div class="api-response hidden space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span id="response-status" class="font-semibold" style="color: var(--text-primary);"></span>
|
||||
<span id="response-time" style="color: var(--text-secondary);"></span>
|
||||
</div>
|
||||
<h4 class="text-text-secondary mb-2 text-sm font-medium">Request Body</h4>
|
||||
<textarea id="request-body" placeholder="Edit request body..." class="w-full min-h-[150px] px-3 py-2 font-mono text-sm bg-background-primary text-text-primary border border-border rounded focus:outline-none focus:ring-2 focus:ring-accent">{ explorer.Endpoint.RequestBody }</textarea>
|
||||
<pre class="card p-4 overflow-x-auto"><code id="response-body" class="text-sm" style="color: var(--text-primary);"></code></pre>
|
||||
</div>
|
||||
<button id="try-it-out" @click="tryDocEndpoint()" class="bg-accent text-white px-6 py-3 rounded cursor-pointer mt-4 hover:opacity-90">Try It Out</button>
|
||||
<div class="api-response mt-4 hidden">
|
||||
<div class="flex justify-between mb-2 text-sm">
|
||||
<span id="response-status" class="font-semibold"></span>
|
||||
<span id="response-time" class="text-text-secondary"></span>
|
||||
</div>
|
||||
<pre class="bg-background-primary p-4 rounded-lg overflow-x-auto"><code id="response-body" class="text-text-primary text-sm"></code></pre>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4 flex-wrap">
|
||||
<button @click="copyDocRequest()" class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm">Copy Request</button>
|
||||
<button @click="copyDocResponse()" class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm">Copy Response</button>
|
||||
<button @click="generateDocCURL()" class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm">Generate cURL</button>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button @click="copyDocRequest()" class="btn btn-secondary text-sm">
|
||||
@Icon("copy", "h-4 w-4")
|
||||
Copy Request
|
||||
</button>
|
||||
<button @click="copyDocResponse()" class="btn btn-secondary text-sm">
|
||||
@Icon("copy", "h-4 w-4")
|
||||
Copy Response
|
||||
</button>
|
||||
<button @click="generateDocCURL()" class="btn btn-secondary text-sm">
|
||||
@Icon("external", "h-4 w-4")
|
||||
Generate cURL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
+238
-50
@@ -30,131 +30,319 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if !explorer.IsLoggedIn {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " <div x-data=\"devices\" class=\"border border-border rounded-lg p-6 mt-8 bg-background-secondary\"><div class=\"flex gap-2 mb-4\"><button class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary cursor-pointer bg-accent text-white\">Mock Data</button> <button class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary opacity-50 cursor-not-allowed\" disabled>Login to Try Real</button></div><div class=\"mb-4\"><div class=\"mb-4\"><select disabled class=\"w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent\"><option value=\"GET\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"devices\" class=\"card p-6 mt-8 space-y-5\"><div class=\"flex items-center gap-2 flex-wrap\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if explorer.Endpoint.Method == "GET" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<span class=\"badge status-completed\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 8, Col: 68}
|
||||
}
|
||||
_, 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, 3, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if explorer.Endpoint.Method == "DELETE" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"badge status-failed\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 10, Col: 65}
|
||||
}
|
||||
_, 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, 5, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if explorer.Endpoint.Method == "POST" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 12, Col: 120}
|
||||
}
|
||||
_, 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, 7, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"badge status-processing\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 14, Col: 69}
|
||||
}
|
||||
_, 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, 9, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, ">GET</option> <option value=\"POST\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<code class=\"text-sm\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if explorer.Endpoint.Method == "POST" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Path)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 16, Col: 88}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">POST</option> <option value=\"PUT\"")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if explorer.Endpoint.Method == "PUT" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">PUT</option> <option value=\"DELETE\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</code></div><div class=\"flex gap-2\"><button class=\"btn btn-primary\">Mock Data</button> <button class=\"btn btn-secondary opacity-50 cursor-not-allowed\" disabled>Login to Try Real</button></div><div><p class=\"text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary);\">Request Body (Example)</p><pre class=\"card p-4 overflow-x-auto\"><code class=\"text-sm\" style=\"color: var(--text-primary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if explorer.Endpoint.Method == "DELETE" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 24, Col: 131}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">DELETE</option></select></div><h4 class=\"text-text-secondary mb-2 text-sm font-medium\">Request Body (Example)</h4><pre class=\"bg-background-primary p-4 rounded-lg overflow-x-auto\"><code class=\"text-text-primary text-sm\">")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 21, Col: 141}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</code></pre></div><div><p class=\"text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary);\">Response (Mock)</p><pre class=\"card p-4 overflow-x-auto\"><code class=\"text-sm\" style=\"color: var(--text-primary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</code></pre></div><div class=\"mt-4\"><h4 class=\"text-text-secondary mb-2 text-sm font-medium\">Response (Mock)</h4><pre class=\"bg-background-primary p-4 rounded-lg overflow-x-auto\"><code class=\"text-text-primary text-sm\">")
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Response)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 28, Col: 128}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Response)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 25, Col: 138}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</code></pre></div><div class=\"flex gap-2\"><button @click=\"copyToClipboard(JSON.stringify({ explorer.Endpoint.RequestBody }, null, 2))\" class=\"btn btn-secondary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</code></pre></div><div class=\"flex gap-2 mt-4\"><button @click=\"copyToClipboard(JSON.stringify({ explorer.Endpoint.RequestBody }, null, 2))\" class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm\">Copy Request</button> <button @click=\"copyToClipboard(JSON.stringify({ explorer.Endpoint.Response }, null, 2))\" class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm\">Copy Response</button></div></div>")
|
||||
templ_7745c5c3_Err = Icon("copy", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Copy Request</button> <button @click=\"copyToClipboard(JSON.stringify({ explorer.Endpoint.Response }, null, 2))\" class=\"btn btn-secondary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("copy", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Copy Response</button></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " <div class=\"border border-border rounded-lg p-6 mt-8 bg-background-secondary\" x-data=\"apiExplorerDoc\" x-init=\"initAPIExplorerDoc('{ explorer.Endpoint.Path }', '{ explorer.Endpoint.RequestBody }', '{ explorer.Endpoint.Response }')\"><div class=\"flex gap-2 mb-4\"><button class=\"mode-btn px-4 py-2 border border-border rounded bg-background-primary text-text-primary cursor-pointer hover:bg-background-secondary text-sm\" id=\"mock-btn\" @click=\"showDocMode('mock')\">Mock Data</button> <button class=\"mode-btn px-4 py-2 bg-accent text-white rounded cursor-pointer text-sm\" id=\"real-btn\" @click=\"showDocMode('real')\">Try It Out</button></div><div class=\"mb-4\"><div class=\"mb-4\"><select id=\"http-method\" class=\"w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent\"><option value=\"GET\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"card p-6 mt-8 space-y-5\" x-data=\"apiExplorerDoc\" x-init=\"initAPIExplorerDoc('{ explorer.Endpoint.Path }', '{ explorer.Endpoint.RequestBody }', '{ explorer.Endpoint.Response }')\"><div class=\"flex items-center gap-2 flex-wrap\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if explorer.Endpoint.Method == "GET" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"badge status-completed\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 45, Col: 68}
|
||||
}
|
||||
_, 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, 18, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if explorer.Endpoint.Method == "DELETE" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"badge status-failed\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 47, Col: 65}
|
||||
}
|
||||
_, 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, 20, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if explorer.Endpoint.Method == "POST" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 49, Col: 120}
|
||||
}
|
||||
_, 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, 22, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span class=\"badge status-processing\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 51, Col: 69}
|
||||
}
|
||||
_, 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, 24, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ">GET</option> <option value=\"POST\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<code class=\"text-sm\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Path)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 53, Col: 88}
|
||||
}
|
||||
_, 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, 26, "</code></div><div class=\"flex gap-2\"><button class=\"mode-btn btn btn-secondary text-sm\" id=\"mock-btn\" @click=\"showDocMode('mock')\">Mock Data</button> <button class=\"mode-btn btn btn-primary text-sm\" id=\"real-btn\" @click=\"showDocMode('real')\">Try It Out</button></div><div><label for=\"http-method\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary);\">Method</label> <select id=\"http-method\" class=\"input\"><option value=\"GET\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if explorer.Endpoint.Method == "GET" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, ">GET</option> <option value=\"POST\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if explorer.Endpoint.Method == "POST" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, ">POST</option> <option value=\"PUT\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, ">POST</option> <option value=\"PUT\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if explorer.Endpoint.Method == "PUT" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, ">PUT</option> <option value=\"DELETE\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, ">PUT</option> <option value=\"DELETE\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if explorer.Endpoint.Method == "DELETE" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, ">DELETE</option></select></div><h4 class=\"text-text-secondary mb-2 text-sm font-medium\">Request Body</h4><textarea id=\"request-body\" placeholder=\"Edit request body...\" class=\"w-full min-h-[150px] px-3 py-2 font-mono text-sm bg-background-primary text-text-primary border border-border rounded focus:outline-none focus:ring-2 focus:ring-accent\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ">DELETE</option></select></div><div><label for=\"request-body\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary);\">Request Body</label> <textarea id=\"request-body\" placeholder=\"Edit request body…\" class=\"input min-h-[150px] font-mono\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody)
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 49, Col: 274}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 70, Col: 136}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
_, 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, 21, "</textarea></div><button id=\"try-it-out\" @click=\"tryDocEndpoint()\" class=\"bg-accent text-white px-6 py-3 rounded cursor-pointer mt-4 hover:opacity-90\">Try It Out</button><div class=\"api-response mt-4 hidden\"><div class=\"flex justify-between mb-2 text-sm\"><span id=\"response-status\" class=\"font-semibold\"></span> <span id=\"response-time\" class=\"text-text-secondary\"></span></div><pre class=\"bg-background-primary p-4 rounded-lg overflow-x-auto\"><code id=\"response-body\" class=\"text-text-primary text-sm\"></code></pre></div><div class=\"flex gap-2 mt-4 flex-wrap\"><button @click=\"copyDocRequest()\" class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm\">Copy Request</button> <button @click=\"copyDocResponse()\" class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm\">Copy Response</button> <button @click=\"generateDocCURL()\" class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm\">Generate cURL</button></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</textarea></div><button id=\"try-it-out\" @click=\"tryDocEndpoint()\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("play", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Try It Out</button><div class=\"api-response hidden space-y-2\"><div class=\"flex justify-between text-sm\"><span id=\"response-status\" class=\"font-semibold\" style=\"color: var(--text-primary);\"></span> <span id=\"response-time\" style=\"color: var(--text-secondary);\"></span></div><pre class=\"card p-4 overflow-x-auto\"><code id=\"response-body\" class=\"text-sm\" style=\"color: var(--text-primary);\"></code></pre></div><div class=\"flex gap-2 flex-wrap\"><button @click=\"copyDocRequest()\" class=\"btn btn-secondary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("copy", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "Copy Request</button> <button @click=\"copyDocResponse()\" class=\"btn btn-secondary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("copy", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "Copy Response</button> <button @click=\"generateDocCURL()\" class=\"btn btn-secondary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("external", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "Generate cURL</button></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+264
-429
@@ -19,567 +19,403 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body x-data="bookDetail" class="theme-{ user.Theme }" data-format-group={ book.FormatGroup } data-library-id={ uuidToString(book.LibraryID) } data-rating={ fmt.Sprintf("%d", getBookRating(book.Rating)) } data-conflict-id={ conflictID(book.ActiveConflict) } data-conflict-winner={ conflictWinnerSource(book.ActiveConflict) }>
|
||||
<body
|
||||
x-data="bookDetail"
|
||||
class="theme-{ user.Theme }"
|
||||
data-format-group={ book.FormatGroup }
|
||||
data-library-id={ uuidToString(book.LibraryID) }
|
||||
data-rating={ fmt.Sprintf("%d", getBookRating(book.Rating)) }
|
||||
data-conflict-id={ conflictID(book.ActiveConflict) }
|
||||
data-conflict-winner={ conflictWinnerSource(book.ActiveConflict) }
|
||||
>
|
||||
@Header(user, "/media/{ uuidToString(book.ID) }")
|
||||
<div class="sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b" style="background-color: var(--bg-primary);">
|
||||
<div class="w-full px-4 py-3 flex items-center gap-4">
|
||||
<a id="back-link" href="/dashboard" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
← Back
|
||||
<!-- Hero with blurred cover backdrop -->
|
||||
<div class="relative overflow-hidden">
|
||||
if book.CoverImagePath.Valid && book.CoverImagePath.String != "" {
|
||||
<div class="hero-backdrop" style={ "background-image: url('" + book.CoverImagePath.String + "')" }></div>
|
||||
}
|
||||
<div class="relative z-10 xs:w-full md:mx-auto md:container px-4 sm:px-6 lg:px-8 pt-6 pb-8">
|
||||
<a id="back-link" href="/dashboard" class="inline-flex items-center gap-1.5 text-sm font-medium hover:underline transition-colors mb-6" style="color: var(--text-secondary);">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
<span>Back</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="xs:w-full md:mx-auto md:container px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Top Section: Cover + Basic Info + Actions -->
|
||||
<div class="flex flex-col md:flex-row gap-8 mb-8">
|
||||
<!-- Left: Cover Image (256x384px) -->
|
||||
<div class="flex-shrink-0">
|
||||
if book.CoverImagePath.Valid && book.CoverImagePath.String != "" {
|
||||
<img
|
||||
src={ book.CoverImagePath.String }
|
||||
alt={ book.Title }
|
||||
class="w-64 h-96 object-cover rounded-lg shadow-xl"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
/>
|
||||
} else {
|
||||
<img
|
||||
src="/static/placeholder-book.svg"
|
||||
alt="{ book.Title }"
|
||||
class="w-64 h-96 object-cover rounded-lg shadow-xl"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<!-- Right: Details -->
|
||||
<div class="flex-1">
|
||||
<!-- Title & Author -->
|
||||
<h1 class="text-4xl font-bold mb-2" style="color: var(--text-primary)">{ book.Title }</h1>
|
||||
if book.Author.Valid && book.Author.String != "" {
|
||||
<h2 class="text-2xl mb-6" style="color: var(--text-secondary)">
|
||||
by { book.Author.String }
|
||||
</h2>
|
||||
}
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<!-- Read Now (placeholder) -->
|
||||
<button
|
||||
@click={ "window.location.href = '/readers/" + uuidToString(book.ID) + "'" }
|
||||
class="px-6 py-3 rounded-lg font-semibold"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
>
|
||||
📖 Read Now
|
||||
</button>
|
||||
<!-- Mark as Read / Unread toggle -->
|
||||
if book.ReadingProgress != nil && book.ReadingProgress.Percentage.Valid && book.ReadingProgress.Percentage.Float64 >= 1.0 {
|
||||
<button
|
||||
@click="toggleRead(false)"
|
||||
:disabled="readSaving"
|
||||
class="px-6 py-3 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary); background-color: var(--bg-secondary);"
|
||||
>
|
||||
<span x-show="!readSaving">✅ Mark as Unread</span>
|
||||
<svg x-show="readSaving" class="animate-spin inline-block h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex flex-col md:flex-row gap-8">
|
||||
<!-- Cover -->
|
||||
<div class="flex-shrink-0 mx-auto md:mx-0">
|
||||
if book.CoverImagePath.Valid && book.CoverImagePath.String != "" {
|
||||
<img
|
||||
src={ book.CoverImagePath.String }
|
||||
alt={ book.Title }
|
||||
class="w-48 md:w-56 h-72 md:h-80 object-cover rounded-2xl"
|
||||
style="box-shadow: var(--shadow-pop);"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
/>
|
||||
} else {
|
||||
<button
|
||||
@click="toggleRead(true)"
|
||||
:disabled="readSaving"
|
||||
class="px-6 py-3 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary); background-color: var(--bg-secondary);"
|
||||
>
|
||||
<span x-show="!readSaving">📖 Mark as Read</span>
|
||||
<svg x-show="readSaving" class="animate-spin inline-block h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<img
|
||||
src="/static/placeholder-book.svg"
|
||||
alt={ book.Title }
|
||||
class="w-48 md:w-56 h-72 md:h-80 object-cover rounded-2xl"
|
||||
style="box-shadow: var(--shadow-pop);"
|
||||
/>
|
||||
}
|
||||
<!-- Sync Progress -->
|
||||
if book.ActiveConflict != nil || book.ReadingProgress != nil {
|
||||
<button
|
||||
@click="showProgressSyncModal()"
|
||||
class="px-6 py-3 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary); background-color: var(--bg-secondary);"
|
||||
>
|
||||
🔄 Sync Progress
|
||||
</button>
|
||||
</div>
|
||||
<!-- Details -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="text-3xl md:text-4xl font-bold tracking-tight mb-2" style="color: var(--text-primary)">{ book.Title }</h1>
|
||||
if book.Author.Valid && book.Author.String != "" {
|
||||
<h2 class="text-lg md:text-xl mb-5" style="color: var(--text-secondary)">
|
||||
by { book.Author.String }
|
||||
</h2>
|
||||
}
|
||||
<!-- View Notes/Highlights -->
|
||||
<button
|
||||
@click="showNotesModal()"
|
||||
class="px-6 py-3 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary); background-color: var(--bg-secondary);"
|
||||
>
|
||||
📝 Notes & Highlights
|
||||
if book.NotesCount + book.HighlightsCount > 0 {
|
||||
<span
|
||||
class="ml-2 px-2 py-0.5 rounded text-xs font-semibold"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
>
|
||||
{ book.NotesCount + book.HighlightsCount }
|
||||
</span>
|
||||
<!-- Action buttons -->
|
||||
<div class="flex flex-wrap gap-2.5 mb-6">
|
||||
<button
|
||||
@click={ "window.location.href = '/readers/" + uuidToString(book.ID) + "'" }
|
||||
class="btn btn-primary px-5 py-2.5"
|
||||
>
|
||||
@Icon("book-open", "h-4 w-4")
|
||||
<span>Read Now</span>
|
||||
</button>
|
||||
if book.ReadingProgress != nil && book.ReadingProgress.Percentage.Valid && book.ReadingProgress.Percentage.Float64 >= 1.0 {
|
||||
<button @click="toggleRead(false)" :disabled="readSaving" class="btn btn-secondary px-5 py-2.5">
|
||||
<span x-show="!readSaving" class="inline-flex items-center gap-2">@Icon("check-circle", "h-4 w-4")<span>Mark Unread</span></span>
|
||||
<svg x-show="readSaving" class="animate-spin inline-block h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
} else {
|
||||
<button @click="toggleRead(true)" :disabled="readSaving" class="btn btn-secondary px-5 py-2.5">
|
||||
<span x-show="!readSaving" class="inline-flex items-center gap-2">@Icon("check", "h-4 w-4")<span>Mark Read</span></span>
|
||||
<svg x-show="readSaving" class="animate-spin inline-block h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</button>
|
||||
<!-- Edit Metadata (placeholder) -->
|
||||
<button
|
||||
@click="showMetadataEditor()"
|
||||
class="px-6 py-3 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary); background-color: var(--bg-secondary);"
|
||||
>
|
||||
✏️ Edit Metadata
|
||||
</button>
|
||||
</div>
|
||||
<!-- Rating Display -->
|
||||
<div class="mb-6" @mouseleave="ratingHover = 0">
|
||||
<span class="text-2xl">
|
||||
<template x-for="i in 5" :key="i">
|
||||
<span style="position: relative; display: inline-block;">
|
||||
<span :style="starFill(i)">★</span>
|
||||
<button
|
||||
type="button"
|
||||
@click="setRating(i*2-1)"
|
||||
@mouseenter="ratingHover = i*2-1"
|
||||
:disabled="ratingSaving"
|
||||
:aria-label="'Rate ' + ((i*2-1)/2) + ' of 5 stars'"
|
||||
style="position: absolute; left: 0; top: 0; width: 50%; height: 100%; background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer;"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
@click="setRating(i*2)"
|
||||
@mouseenter="ratingHover = i*2"
|
||||
:disabled="ratingSaving"
|
||||
:aria-label="'Rate ' + ((i*2)/2) + ' of 5 stars'"
|
||||
style="position: absolute; right: 0; top: 0; width: 50%; height: 100%; background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer;"
|
||||
></button>
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
<span class="ml-2 text-sm align-middle" style="color: var(--text-secondary);" x-text="ratingText()"></span>
|
||||
<button
|
||||
type="button"
|
||||
x-show="userRating > 0 && !ratingSaving"
|
||||
@click="clearRating()"
|
||||
class="ml-2 text-xs underline hover:opacity-70"
|
||||
style="color: var(--text-secondary);"
|
||||
>Clear</button>
|
||||
<svg x-show="ratingSaving" class="animate-spin inline-block h-4 w-4 ml-1 align-middle" viewBox="0 0 24 24" fill="none" style="color: var(--text-secondary);">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Community Rating Display (from metadata) -->
|
||||
if book.CommunityRating.Valid && book.CommunityRating.Float64 > 0 {
|
||||
<div class="mb-2">
|
||||
<span class="text-lg" style="color: var(--text-secondary);">
|
||||
Community Rating:
|
||||
<span class="font-bold" style="color: var(--text-primary);">
|
||||
if book.ActiveConflict != nil || book.ReadingProgress != nil {
|
||||
<button @click="showProgressSyncModal()" class="btn btn-secondary px-5 py-2.5">
|
||||
@Icon("sync", "h-4 w-4")
|
||||
<span>Sync Progress</span>
|
||||
</button>
|
||||
}
|
||||
<button @click="showNotesModal()" class="btn btn-secondary px-5 py-2.5">
|
||||
@Icon("edit", "h-4 w-4")
|
||||
<span>Notes</span>
|
||||
if book.NotesCount + book.HighlightsCount > 0 {
|
||||
<span class="badge ml-1" style="background-color: var(--accent-muted); color: var(--accent);">{ book.NotesCount + book.HighlightsCount }</span>
|
||||
}
|
||||
</button>
|
||||
<button @click="showMetadataEditor()" class="btn btn-secondary px-5 py-2.5">
|
||||
@Icon("edit", "h-4 w-4")
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Rating -->
|
||||
<div class="mb-5" @mouseleave="ratingHover = 0">
|
||||
<span class="text-2xl">
|
||||
<template x-for="i in 5" :key="i">
|
||||
<span style="position: relative; display: inline-block;">
|
||||
<span :style="starFill(i)">★</span>
|
||||
<button
|
||||
type="button"
|
||||
@click="setRating(i*2-1)"
|
||||
@mouseenter="ratingHover = i*2-1"
|
||||
:disabled="ratingSaving"
|
||||
:aria-label="'Rate ' + ((i*2-1)/2) + ' of 5 stars'"
|
||||
style="position: absolute; left: 0; top: 0; width: 50%; height: 100%; background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer;"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
@click="setRating(i*2)"
|
||||
@mouseenter="ratingHover = i*2"
|
||||
:disabled="ratingSaving"
|
||||
:aria-label="'Rate ' + ((i*2)/2) + ' of 5 stars'"
|
||||
style="position: absolute; right: 0; top: 0; width: 50%; height: 100%; background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer;"
|
||||
></button>
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
<span class="ml-2 text-sm align-middle" style="color: var(--text-secondary);" x-text="ratingText()"></span>
|
||||
<button
|
||||
type="button"
|
||||
x-show="userRating > 0 && !ratingSaving"
|
||||
@click="clearRating()"
|
||||
class="ml-2 text-xs underline hover:opacity-70"
|
||||
style="color: var(--text-secondary);"
|
||||
>Clear</button>
|
||||
<svg x-show="ratingSaving" class="animate-spin inline-block h-4 w-4 ml-1 align-middle" viewBox="0 0 24 24" fill="none" style="color: var(--text-secondary);">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Community rating -->
|
||||
if book.CommunityRating.Valid && book.CommunityRating.Float64 > 0 {
|
||||
<div class="mb-3 text-sm" style="color: var(--text-secondary);">
|
||||
Community:
|
||||
<span class="font-bold ml-1" style="color: var(--text-primary);">
|
||||
@templ.Raw(renderStars(int32(book.CommunityRating.Float64)))
|
||||
</span>
|
||||
<span class="ml-2 text-sm" style="color: var(--text-secondary);">
|
||||
{ fmt.Sprintf("%.1f / 10", book.CommunityRating.Float64) }
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
<!-- Series Badge -->
|
||||
if book.Series.Valid && book.Series.String != "" {
|
||||
<div class="mb-4">
|
||||
<a
|
||||
href={ "/series/detail?name=" + url.QueryEscape(book.Series.String) + "&library_id=" + uuidToString(book.LibraryID) }
|
||||
class="px-3 py-1 rounded-full text-sm font-semibold inline-block hover:opacity-80 transition-opacity"
|
||||
style="background-color: var(--accent); color: white; text-decoration: none;"
|
||||
>
|
||||
{ book.Series.String }
|
||||
if book.SeriesNumber.Valid && book.SeriesNumber.Int32 > 0 {
|
||||
#{ book.SeriesNumber.Int32 }
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
<!-- Reading Direction Badge (for manga/comics) -->
|
||||
if book.ReadingDirection.Valid && book.ReadingDirection.String != "" && book.ReadingDirection.String != "auto" {
|
||||
<div class="mb-4">
|
||||
<span
|
||||
class="px-3 py-1 rounded-full text-sm font-semibold"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
>
|
||||
📖 { strings.ToUpper(book.ReadingDirection.String) }
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
<!-- Comic-Specific Badges -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<!-- Age Rating Badge -->
|
||||
if book.AgeRating.Valid && book.AgeRating.String != "" {
|
||||
<span
|
||||
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
|
||||
>
|
||||
{ book.AgeRating.String }
|
||||
</span>
|
||||
<span class="ml-2">{ fmt.Sprintf("%.1f / 10", book.CommunityRating.Float64) }</span>
|
||||
</div>
|
||||
}
|
||||
<!-- Black and White Badge -->
|
||||
if book.IsBlackAndWhite.Valid && book.IsBlackAndWhite.Bool {
|
||||
<span
|
||||
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
|
||||
>
|
||||
B&W
|
||||
</span>
|
||||
}
|
||||
<!-- Story Arc Badge -->
|
||||
if book.StoryArc.Valid && book.StoryArc.String != "" {
|
||||
<span
|
||||
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
|
||||
>
|
||||
📚 { book.StoryArc.String }
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<!-- Tags -->
|
||||
if len(book.Tags) > 0 {
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
for _, tag := range book.Tags {
|
||||
<!-- Badges: series, direction, tags -->
|
||||
<div class="flex flex-wrap items-center gap-2 mb-3">
|
||||
if book.Series.Valid && book.Series.String != "" {
|
||||
<a
|
||||
href={ "/tags/detail?name=" + url.QueryEscape(tag) + "&library_id=" + uuidToString(book.LibraryID) }
|
||||
class="px-2 py-1 rounded-full text-xs font-semibold hover:opacity-80 transition-opacity"
|
||||
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary); text-decoration: none;"
|
||||
href={ "/series/detail?name=" + url.QueryEscape(book.Series.String) + "&library_id=" + uuidToString(book.LibraryID) }
|
||||
class="chip hover:opacity-90"
|
||||
style="background-color: var(--accent); color: var(--bg-primary); text-decoration: none;"
|
||||
>
|
||||
{ tag }
|
||||
{ book.Series.String }
|
||||
if book.SeriesNumber.Valid && book.SeriesNumber.Int32 > 0 {
|
||||
#{ book.SeriesNumber.Int32 }
|
||||
}
|
||||
</a>
|
||||
}
|
||||
if book.ReadingDirection.Valid && book.ReadingDirection.String != "" && book.ReadingDirection.String != "auto" {
|
||||
<span class="chip" style="background-color: var(--accent); color: var(--bg-primary);">{ strings.ToUpper(book.ReadingDirection.String) }</span>
|
||||
}
|
||||
if book.AgeRating.Valid && book.AgeRating.String != "" {
|
||||
<span class="badge" style="border: 1px solid var(--border); color: var(--text-secondary);">{ book.AgeRating.String }</span>
|
||||
}
|
||||
if book.IsBlackAndWhite.Valid && book.IsBlackAndWhite.Bool {
|
||||
<span class="badge" style="border: 1px solid var(--border); color: var(--text-secondary);">B&W</span>
|
||||
}
|
||||
if book.StoryArc.Valid && book.StoryArc.String != "" {
|
||||
<span class="badge" style="border: 1px solid var(--border); color: var(--text-secondary);">{ book.StoryArc.String }</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- Description/Synopsis -->
|
||||
if book.Description.Valid && book.Description.String != "" {
|
||||
<div class="mb-6">
|
||||
<h3 class="font-semibold mb-2" style="color: var(--text-primary)">Synopsis</h3>
|
||||
<div class="max-h-[20rem] overflow-y-auto pr-2" style="color: var(--text-secondary)">
|
||||
@UnsafeHTML(
|
||||
bluemonday.UGCPolicy().Sanitize(book.Description.String),
|
||||
).ToComponent()
|
||||
if len(book.Tags) > 0 {
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
for _, tag := range book.Tags {
|
||||
<a
|
||||
href={ "/tags/detail?name=" + url.QueryEscape(tag) + "&library_id=" + uuidToString(book.LibraryID) }
|
||||
class="badge hover:opacity-80 transition-opacity"
|
||||
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary); text-decoration: none;"
|
||||
>
|
||||
{ tag }
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Summary (from ComicInfo.xml, if different from description) -->
|
||||
</div>
|
||||
<div class="xs:w-full md:mx-auto md:container px-4 sm:px-6 lg:px-8 pb-10">
|
||||
<!-- Synopsis -->
|
||||
if book.Description.Valid && book.Description.String != "" {
|
||||
<div class="card p-6 mb-6">
|
||||
<h3 class="font-semibold mb-2" style="color: var(--text-primary)">Synopsis</h3>
|
||||
<div class="max-h-[20rem] overflow-y-auto pr-2 text-sm leading-relaxed" style="color: var(--text-secondary)">
|
||||
@UnsafeHTML(bluemonday.UGCPolicy().Sanitize(book.Description.String)).ToComponent()
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if book.Summary.Valid && book.Summary.String != "" && book.Summary.String != book.Description.String {
|
||||
<div class="mb-6">
|
||||
<div class="card p-6 mb-6">
|
||||
<h3 class="font-semibold mb-2" style="color: var(--text-primary)">Comic Summary</h3>
|
||||
<div class="max-h-[20rem] overflow-y-auto pr-2" style="color: var(--text-secondary)">
|
||||
@UnsafeHTML(
|
||||
bluemonday.UGCPolicy().Sanitize(book.Summary.String),
|
||||
).ToComponent()
|
||||
<div class="max-h-[20rem] overflow-y-auto pr-2 text-sm leading-relaxed" style="color: var(--text-secondary)">
|
||||
@UnsafeHTML(bluemonday.UGCPolicy().Sanitize(book.Summary.String)).ToComponent()
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- Metadata Notes (technical notes from metadata files) -->
|
||||
if book.MetadataNotes.Valid && book.MetadataNotes.String != "" {
|
||||
<div
|
||||
class="card p-6 rounded-lg border mb-6"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||
>
|
||||
<div class="card p-6 mb-6">
|
||||
<h3 class="font-semibold mb-2" style="color: var(--text-primary)">Metadata Notes</h3>
|
||||
<div class="text-sm" style="color: var(--text-secondary);">
|
||||
{ book.MetadataNotes.String }
|
||||
</div>
|
||||
<div class="text-sm" style="color: var(--text-secondary);">{ book.MetadataNotes.String }</div>
|
||||
</div>
|
||||
}
|
||||
<!-- Progress Section -->
|
||||
<!-- Progress -->
|
||||
if book.ReadingProgress != nil {
|
||||
<div
|
||||
class="card p-6 rounded-lg border mb-6"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||
>
|
||||
<div class="card p-6 mb-6">
|
||||
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Reading Progress</h3>
|
||||
if book.ActiveConflict != nil {
|
||||
<div
|
||||
class="mb-4 p-3 rounded-lg border"
|
||||
style="background-color: #f59e0b20; border-color: #f59e0b;"
|
||||
>
|
||||
<p style="color: var(--text-primary);">
|
||||
⚠️ Progress conflict detected - Click "Sync Progress" to review and resolve
|
||||
</p>
|
||||
<div class="mb-4 p-3 rounded-xl flex items-start gap-2" style="background-color: color-mix(in srgb, var(--status-warning) 14%, transparent); border: 1px solid var(--status-warning);">
|
||||
@Icon("alert", "h-5 w-5 shrink-0")
|
||||
<p class="text-sm" style="color: var(--text-primary);">Progress conflict detected — click "Sync Progress" to review and resolve.</p>
|
||||
</div>
|
||||
}
|
||||
<!-- Progress Bar -->
|
||||
<div class="mb-4">
|
||||
<div class="w-full rounded-full h-3" style="background-color: var(--bg-primary);">
|
||||
<div class="w-full rounded-full h-2.5" style="background-color: var(--bg-primary);">
|
||||
<div
|
||||
class="h-3 rounded-full transition-all"
|
||||
class="h-2.5 rounded-full transition-all"
|
||||
style={ fmt.Sprintf("width: %.1f%%; background-color: var(--accent);", book.ReadingProgress.Percentage.Float64*100) }
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Progress Stats Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Progress</p>
|
||||
<p class="text-2xl font-bold" style="color: var(--accent);">
|
||||
{ fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64 * 100) }%
|
||||
</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Progress</p>
|
||||
<p class="text-2xl font-bold" style="color: var(--accent);">{ fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64*100) }%</p>
|
||||
</div>
|
||||
if book.ReadingProgress.CurrentPage.Valid && book.ReadingProgress.TotalPages.Valid {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Page</p>
|
||||
<p>{ book.ReadingProgress.CurrentPage.Int32 } / { book.ReadingProgress.TotalPages.Int32 }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Page</p>
|
||||
<p style="color: var(--text-primary)">{ book.ReadingProgress.CurrentPage.Int32 } / { book.ReadingProgress.TotalPages.Int32 }</p>
|
||||
</div>
|
||||
}
|
||||
if book.ReadingProgress.LastReadAt.Valid {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Last Read</p>
|
||||
<p>{ FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Last Read</p>
|
||||
<p class="text-sm" style="color: var(--text-primary)">{ FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) }</p>
|
||||
</div>
|
||||
}
|
||||
if book.ReadingProgress.LastSyncSource.Valid {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Source</p>
|
||||
<p class="capitalize">{ book.ReadingProgress.LastSyncSource.String }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Source</p>
|
||||
<p class="capitalize" style="color: var(--text-primary)">{ book.ReadingProgress.LastSyncSource.String }</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- Metadata Grid -->
|
||||
<div
|
||||
class="card p-6 rounded-lg border mb-6"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||
>
|
||||
<!-- Metadata grid -->
|
||||
<div class="card p-6 mb-6">
|
||||
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Metadata</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Publication Info -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-4 text-sm">
|
||||
if book.Publisher.Valid && book.Publisher.String != "" {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Publisher</p>
|
||||
<p>{ book.Publisher.String }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Publisher</p>
|
||||
<p style="color: var(--text-primary)">{ book.Publisher.String }</p>
|
||||
</div>
|
||||
}
|
||||
if book.DatePublished.Valid {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Published</p>
|
||||
<p>{ book.DatePublished.Time.Format("01-02-2006") }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Published</p>
|
||||
<p style="color: var(--text-primary)">{ book.DatePublished.Time.Format("01-02-2006") }</p>
|
||||
</div>
|
||||
}
|
||||
if book.Isbn.Valid && book.Isbn.String != "" {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Isbn</p>
|
||||
<p>{ book.Isbn.String }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">ISBN</p>
|
||||
<p style="color: var(--text-primary)">{ book.Isbn.String }</p>
|
||||
</div>
|
||||
}
|
||||
if book.Language.Valid && book.Language.String != "" {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Language</p>
|
||||
<p class="capitalize">{ book.Language.String }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Language</p>
|
||||
<p class="capitalize" style="color: var(--text-primary)">{ book.Language.String }</p>
|
||||
</div>
|
||||
}
|
||||
if book.Edition.Valid && book.Edition.String != "" {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Edition</p>
|
||||
<p>{ book.Edition.String }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Edition</p>
|
||||
<p style="color: var(--text-primary)">{ book.Edition.String }</p>
|
||||
</div>
|
||||
}
|
||||
if book.PageCount.Valid && book.PageCount.Int32 > 0 {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Pages</p>
|
||||
<p>{ book.PageCount.Int32 }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Pages</p>
|
||||
<p style="color: var(--text-primary)">{ book.PageCount.Int32 }</p>
|
||||
</div>
|
||||
}
|
||||
if book.Genre.Valid && book.Genre.String != "" {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Genre</p>
|
||||
<p>{ book.Genre.String }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Genre</p>
|
||||
<p style="color: var(--text-primary)">{ book.Genre.String }</p>
|
||||
</div>
|
||||
}
|
||||
<!-- Series Information (if not shown in badge) -->
|
||||
if book.SeriesCount.Valid && book.SeriesCount.Int32 > 0 {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Series Count</p>
|
||||
<p>{ book.SeriesCount.Int32 } items</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Series Count</p>
|
||||
<p style="color: var(--text-primary)">{ book.SeriesCount.Int32 } items</p>
|
||||
</div>
|
||||
}
|
||||
if book.Volume.Valid && book.Volume.Int32 > 0 {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Volume</p>
|
||||
<p>Vol. { book.Volume.Int32 }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Volume</p>
|
||||
<p style="color: var(--text-primary)">Vol. { book.Volume.Int32 }</p>
|
||||
</div>
|
||||
}
|
||||
if book.Imprint.Valid && book.Imprint.String != "" {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Imprint</p>
|
||||
<p>{ book.Imprint.String }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Imprint</p>
|
||||
<p style="color: var(--text-primary)">{ book.Imprint.String }</p>
|
||||
</div>
|
||||
}
|
||||
if book.CopyrightYear.Valid && book.CopyrightYear.Int32 > 0 {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Copyright Year</p>
|
||||
<p>{ book.CopyrightYear.Int32 }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Copyright</p>
|
||||
<p style="color: var(--text-primary)">{ book.CopyrightYear.Int32 }</p>
|
||||
</div>
|
||||
}
|
||||
<!-- Comic-Specific Fields -->
|
||||
if book.MangaType.Valid && book.MangaType.String != "" && book.MangaType.String != "unknown" {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Manga Type</p>
|
||||
<p class="capitalize">{ strings.ReplaceAll(book.MangaType.String, "_", " ") }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Manga Type</p>
|
||||
<p class="capitalize" style="color: var(--text-primary)">{ strings.ReplaceAll(book.MangaType.String, "_", " ") }</p>
|
||||
</div>
|
||||
}
|
||||
if book.ScanInformation.Valid && book.ScanInformation.String != "" {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Scan Info</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Scan Info</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary);">{ book.ScanInformation.String }</p>
|
||||
</div>
|
||||
}
|
||||
if len(book.AlternateInfo) > 0 {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Alternate Series</p>
|
||||
<p class="text-sm">{ string(book.AlternateInfo) }</p>
|
||||
</div>
|
||||
}
|
||||
if altSeries := getAlternateSeries(book.AlternateInfo); altSeries != "" {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Alternate Series</p>
|
||||
<p>{ altSeries }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Alternate Series</p>
|
||||
<p style="color: var(--text-primary)">{ altSeries }</p>
|
||||
</div>
|
||||
}
|
||||
if len(book.Contributors) > 0 {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Contributors</p>
|
||||
<p>{ strings.Join(book.Contributors, ", ") }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Contributors</p>
|
||||
<p style="color: var(--text-primary)">{ strings.Join(book.Contributors, ", ") }</p>
|
||||
</div>
|
||||
}
|
||||
<!-- Technical Info -->
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Format</p>
|
||||
<p>{ book.MimeType.String }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Format</p>
|
||||
<p style="color: var(--text-primary)">{ book.MimeType.String }</p>
|
||||
</div>
|
||||
if book.FileSize.Valid && book.FileSize.Int64 > 0 {
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">File Size</p>
|
||||
<p>{ formatFileSize(book.FileSize.Int64) }</p>
|
||||
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">File Size</p>
|
||||
<p style="color: var(--text-primary)">{ formatFileSize(book.FileSize.Int64) }</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- External IDs Section -->
|
||||
if book.GoodreadsID.Valid || book.OpenlibraryID.Valid || book.GoogleBooksID.Valid || book.Asin.Valid || book.Isbn.Valid || book.WebUrl.Valid {
|
||||
<div class="mt-4 pt-4 border-t" style="border-color: var(--border);">
|
||||
<h4 class="text-sm font-semibold mb-3" style="color: var(--text-primary)">External Links</h4>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
if book.GoodreadsID.Valid && book.GoodreadsID.String != "" {
|
||||
<a
|
||||
href={ getExternalURL("goodreads", book.GoodreadsID.String, book.Isbn, book.Title, book.Author) }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm hover:underline flex items-center gap-1"
|
||||
style="color: var(--accent);"
|
||||
>
|
||||
📚 Goodreads
|
||||
</a>
|
||||
} else {
|
||||
<a
|
||||
href={ getExternalURL("goodreads", "", book.Isbn, book.Title, book.Author) }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm hover:underline flex items-center gap-1"
|
||||
style="color: var(--accent);"
|
||||
>
|
||||
📚 Goodreads
|
||||
</a>
|
||||
}
|
||||
if book.OpenlibraryID.Valid && book.OpenlibraryID.String != "" {
|
||||
<a
|
||||
href={ getExternalURL("openlibrary", book.OpenlibraryID.String, book.Isbn, book.Title, book.Author) }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm hover:underline flex items-center gap-1"
|
||||
style="color: var(--accent);"
|
||||
>
|
||||
📖 Open Library
|
||||
</a>
|
||||
} else {
|
||||
<a
|
||||
href={ getExternalURL("openlibrary", "", book.Isbn, book.Title, book.Author) }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm hover:underline flex items-center gap-1"
|
||||
style="color: var(--accent);"
|
||||
>
|
||||
📖 Open Library
|
||||
</a>
|
||||
}
|
||||
if book.GoogleBooksID.Valid && book.GoogleBooksID.String != "" {
|
||||
<a
|
||||
href={ getExternalURL("googlebooks", book.GoogleBooksID.String, book.Isbn, book.Title, book.Author) }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm hover:underline flex items-center gap-1"
|
||||
style="color: var(--accent);"
|
||||
>
|
||||
🔍 Google Books
|
||||
</a>
|
||||
} else {
|
||||
<a
|
||||
href={ getExternalURL("googlebooks", "", book.Isbn, book.Title, book.Author) }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm hover:underline flex items-center gap-1"
|
||||
style="color: var(--accent);"
|
||||
>
|
||||
🔍 Google Books
|
||||
</a>
|
||||
}
|
||||
if book.Asin.Valid && book.Asin.String != "" {
|
||||
<a
|
||||
href={ getExternalURL("amazon", book.Asin.String, book.Isbn, book.Title, book.Author) }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm hover:underline flex items-center gap-1"
|
||||
style="color: var(--accent);"
|
||||
>
|
||||
🛒 Amazon
|
||||
</a>
|
||||
} else if book.Isbn.Valid && book.Isbn.String != "" {
|
||||
<a
|
||||
href={ getExternalURL("amazon", "", book.Isbn, book.Title, book.Author) }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm hover:underline flex items-center gap-1"
|
||||
style="color: var(--accent);"
|
||||
>
|
||||
🛒 Amazon
|
||||
</a>
|
||||
}
|
||||
if book.WebUrl.Valid && book.WebUrl.String != "" {
|
||||
<a
|
||||
href={ book.WebUrl.String }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm hover:underline flex items-center gap-1"
|
||||
style="color: var(--accent);"
|
||||
>
|
||||
🔗 { getDomainName(book.WebUrl.String) }
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="mt-5 pt-4 border-t flex flex-wrap gap-2" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
if book.GoodreadsID.Valid && book.GoodreadsID.String != "" {
|
||||
<a href={ getExternalURL("goodreads", book.GoodreadsID.String, book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Goodreads</a>
|
||||
} else {
|
||||
<a href={ getExternalURL("goodreads", "", book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Goodreads</a>
|
||||
}
|
||||
if book.OpenlibraryID.Valid && book.OpenlibraryID.String != "" {
|
||||
<a href={ getExternalURL("openlibrary", book.OpenlibraryID.String, book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Open Library</a>
|
||||
} else {
|
||||
<a href={ getExternalURL("openlibrary", "", book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Open Library</a>
|
||||
}
|
||||
if book.GoogleBooksID.Valid && book.GoogleBooksID.String != "" {
|
||||
<a href={ getExternalURL("googlebooks", book.GoogleBooksID.String, book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Google Books</a>
|
||||
} else {
|
||||
<a href={ getExternalURL("googlebooks", "", book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Google Books</a>
|
||||
}
|
||||
if book.Asin.Valid && book.Asin.String != "" {
|
||||
<a href={ getExternalURL("amazon", book.Asin.String, book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Amazon</a>
|
||||
} else if book.Isbn.Valid && book.Isbn.String != "" {
|
||||
<a href={ getExternalURL("amazon", "", book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Amazon</a>
|
||||
}
|
||||
if book.WebUrl.Valid && book.WebUrl.String != "" {
|
||||
<a href={ book.WebUrl.String } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">{ getDomainName(book.WebUrl.String) }</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- Collections Section -->
|
||||
<!-- Collections -->
|
||||
if len(book.Collections) > 0 {
|
||||
<div
|
||||
class="card p-6 rounded-lg border mb-6"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||
>
|
||||
<div class="card p-6 mb-6">
|
||||
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Collections</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div class="flex flex-wrap gap-2.5">
|
||||
for _, col := range book.Collections {
|
||||
<a
|
||||
href={ "/collections/" + uuidToString(col.ID) }
|
||||
class="px-3 py-2 rounded-lg border flex items-center gap-2 hover:opacity-80 transition-opacity"
|
||||
style="border-color: { col.Color.String }; background-color: var(--bg-primary); text-decoration: none;"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-xl border hover:opacity-90 transition-opacity"
|
||||
style={ "border-color: " + col.Color.String + "; background-color: var(--bg-primary); text-decoration: none;" }
|
||||
>
|
||||
<span style="color: { col.Color.String };">{ col.Icon.String }</span>
|
||||
<span style={ "color: " + col.Color.String }>{ col.Icon.String }</span>
|
||||
<span style="color: var(--text-primary);">{ col.Name }</span>
|
||||
</a>
|
||||
}
|
||||
@@ -587,7 +423,6 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- Modals -->
|
||||
@ProgressSyncModal(user, book)
|
||||
@NotesHighlightsModal(book)
|
||||
@MetadataEditorModal(book)
|
||||
|
||||
+182
-230
@@ -8,50 +8,52 @@ import (
|
||||
templ ProgressSyncModal(user User, book handlers.MediaDetail) {
|
||||
<div
|
||||
id="progress-sync-modal"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
||||
style="background-color: rgba(0, 0, 0, 0.7);"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4"
|
||||
style="background-color: var(--surface-overlay);"
|
||||
>
|
||||
<div
|
||||
class="card rounded-lg p-6 w-full max-w-4xl mx-4 my-8"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||
class="card w-full max-w-4xl my-8 p-6"
|
||||
style="box-shadow: var(--shadow-pop);"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Sync Progress</h2>
|
||||
<p class="text-sm" style="color: var(--text-secondary);">{ book.Title }</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("sync", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Sync Progress</h2>
|
||||
<p class="text-sm" style="color: var(--text-secondary);">{ book.Title }</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="hideProgressSyncModal()"
|
||||
class="p-2 hover:opacity-80 rounded-lg"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
class="icon-btn"
|
||||
aria-label="Close"
|
||||
>
|
||||
✕
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
if book.ActiveConflict != nil {
|
||||
<div
|
||||
class="mb-6 p-4 rounded-lg border"
|
||||
style="background-color: #f59e0b20; border-color: #f59e0b;"
|
||||
class="mb-6 p-4 rounded-xl flex items-start gap-3"
|
||||
style="background-color: color-mix(in srgb, var(--status-warning) 15%, transparent); border: 1px solid var(--status-warning);"
|
||||
>
|
||||
<p class="font-semibold mb-2" style="color: var(--text-primary);">
|
||||
⚠️ Conflict Detected
|
||||
</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary);">
|
||||
Progress differs between devices. Choose which version to keep.
|
||||
</p>
|
||||
<span class="shrink-0 mt-0.5" style="color: var(--status-warning);">@Icon("alert", "h-5 w-5")</span>
|
||||
<div>
|
||||
<p class="font-semibold mb-1" style="color: var(--text-primary);">
|
||||
Conflict Detected
|
||||
</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary);">
|
||||
Progress differs between devices. Choose which version to keep.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
for source, data := range book.ActiveConflict.ConflictData {
|
||||
<div
|
||||
class="card p-4 rounded-lg border"
|
||||
style="background-color: var(--bg-primary); border-color: var(--border);"
|
||||
>
|
||||
<div class="card p-4">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<span
|
||||
class="inline-block px-2 py-1 rounded text-xs font-semibold capitalize mb-2"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
>
|
||||
<span class="badge mb-2" style="background-color: var(--accent); color: #fff;">
|
||||
{ data.Source }
|
||||
</span>
|
||||
<p class="text-xs" style="color: var(--text-secondary);">
|
||||
@@ -83,8 +85,7 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) {
|
||||
<div class="mt-3">
|
||||
<button
|
||||
@click={"resolveConflict('" + book.ActiveConflict.ID + "', '" + source + "')"}
|
||||
class="px-3 py-1.5 rounded-lg text-sm font-semibold"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Keep This
|
||||
</button>
|
||||
@@ -96,10 +97,7 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) {
|
||||
<div class="mb-4" style="color: var(--text-secondary);">
|
||||
<p>No conflicts detected. Current progress from <strong>{ book.ReadingProgress.LastSyncSource.String }</strong>:</p>
|
||||
</div>
|
||||
<div
|
||||
class="card p-6 rounded-lg border text-center"
|
||||
style="background-color: var(--bg-primary); border-color: var(--border);"
|
||||
>
|
||||
<div class="card p-6 text-center">
|
||||
<div class="text-5xl font-bold mb-4" style="color: var(--accent);">
|
||||
{ fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64 * 100) }%
|
||||
</div>
|
||||
@@ -121,8 +119,7 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) {
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button
|
||||
@click="hideProgressSyncModal()"
|
||||
class="px-4 py-2 rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
@@ -134,14 +131,16 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) {
|
||||
templ NotesHighlightsModal(book handlers.MediaDetail) {
|
||||
<div
|
||||
id="notes-modal"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="background-color: rgba(0, 0, 0, 0.7);"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
style="background-color: var(--surface-overlay);"
|
||||
>
|
||||
<div
|
||||
class="card rounded-lg p-8 w-full max-w-2xl mx-4 text-center"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||
class="card w-full max-w-2xl p-8 text-center"
|
||||
style="box-shadow: var(--shadow-pop);"
|
||||
>
|
||||
<div class="text-6xl mb-4">📝</div>
|
||||
<span class="inline-grid place-items-center h-14 w-14 rounded-2xl mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("edit", "h-7 w-7")
|
||||
</span>
|
||||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Notes & Highlights</h2>
|
||||
<p class="mb-2" style="color: var(--text-secondary);">
|
||||
This book has <strong>{ book.NotesCount }</strong> notes and <strong>{ book.HighlightsCount }</strong> highlights.
|
||||
@@ -150,8 +149,7 @@ templ NotesHighlightsModal(book handlers.MediaDetail) {
|
||||
<div>
|
||||
<button
|
||||
@click="hideNotesModal()"
|
||||
class="px-6 py-2 rounded-lg font-semibold"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
@@ -163,27 +161,31 @@ templ NotesHighlightsModal(book handlers.MediaDetail) {
|
||||
templ MetadataEditorModal(book handlers.MediaDetail) {
|
||||
<div
|
||||
id="metadata-editor-modal"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
||||
style="background-color: rgba(0, 0, 0, 0.7);"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4"
|
||||
style="background-color: var(--surface-overlay);"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg w-full max-w-5xl mx-4 my-8 flex flex-col max-h-[90vh]"
|
||||
style="background-color: var(--bg-secondary); border: 1px solid var(--border);"
|
||||
class="card w-full max-w-5xl my-8 flex flex-col max-h-[90vh]"
|
||||
style="box-shadow: var(--shadow-pop);"
|
||||
>
|
||||
<div class="flex justify-between items-center p-6 border-b flex-shrink-0" style="border-color: var(--border);">
|
||||
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Edit Metadata</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("edit", "h-5 w-5")
|
||||
</span>
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Metadata</h2>
|
||||
</div>
|
||||
<button
|
||||
@click="hideMetadataEditor()"
|
||||
class="p-2 hover:opacity-80 rounded-lg"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
class="icon-btn"
|
||||
aria-label="Close"
|
||||
>
|
||||
✕
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-8 p-6 flex-1 min-h-0 overflow-y-auto">
|
||||
<!-- Cover Section (Left) -->
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-64 h-96 rounded-lg overflow-hidden shadow-lg mb-3 cursor-pointer relative group"
|
||||
<div class="w-64 h-96 rounded-xl overflow-hidden mb-3 cursor-pointer relative group"
|
||||
style="background-color: var(--bg-primary);"
|
||||
@click="document.getElementById('cover-upload-input').click()"
|
||||
>
|
||||
@@ -209,27 +211,26 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
|
||||
<div class="w-64 space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 text-sm rounded-lg font-semibold"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
class="btn btn-primary w-full"
|
||||
@click="generateCover()"
|
||||
x-show="coverGenerating"
|
||||
disabled
|
||||
>
|
||||
@Icon("refresh", "h-4 w-4")
|
||||
Generating...
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 text-sm rounded-lg font-semibold"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
class="btn btn-primary w-full"
|
||||
@click="generateCover()"
|
||||
x-show="!coverGenerating"
|
||||
>
|
||||
@Icon("refresh", "h-4 w-4")
|
||||
Generate Cover
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 text-sm rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border);"
|
||||
class="btn btn-ghost w-full"
|
||||
@click="removeCover()"
|
||||
x-show="hasExistingCover || newCoverPreview"
|
||||
>
|
||||
@@ -237,53 +238,43 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Accordion Fields (Right) -->
|
||||
<div class="flex-1 min-w-0 space-y-2">
|
||||
<!-- Basic Info -->
|
||||
<div class="border rounded-lg" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
||||
style="color: var(--text-primary);"
|
||||
@click="toggleSection('basic')">
|
||||
<span class="font-semibold">Basic Info</span>
|
||||
<span x-text="openSections.basic ? '▾' : '▸'">▸</span>
|
||||
<span class="inline-flex">
|
||||
<span x-show="openSections.basic">@Icon("chevron-down", "h-4 w-4")</span>
|
||||
<span x-show="!openSections.basic" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
||||
</span>
|
||||
</button>
|
||||
<div x-show="openSections.basic" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
||||
<div x-show="openSections.basic" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Title</label>
|
||||
<input type="text" name="title" value={ book.Title }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Title</label>
|
||||
<input type="text" name="title" value={ book.Title } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Author</label>
|
||||
<input type="text" name="author" value={ textToString(book.Author) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Author</label>
|
||||
<input type="text" name="author" value={ textToString(book.Author) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Description</label>
|
||||
<textarea name="description" rows="3"
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);"
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Description</label>
|
||||
<textarea name="description" rows="3" class="input"
|
||||
>{ textToString(book.Description) }</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Summary</label>
|
||||
<textarea name="summary" rows="2"
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);"
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Summary</label>
|
||||
<textarea name="summary" rows="2" class="input"
|
||||
>{ textToString(book.Summary) }</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Tags</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Tags</label>
|
||||
<div class="flex flex-wrap gap-1.5 mb-2">
|
||||
<template x-for="(tag, idx) in editorTags" :key="idx">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
>
|
||||
<span class="chip">
|
||||
<span x-text="tag"></span>
|
||||
<button type="button" class="hover:opacity-70 leading-none" @click="removeEditorTag(idx)">✕</button>
|
||||
<button type="button" class="hover:bg-surface-hover rounded leading-none" @click="removeEditorTag(idx)">@Icon("close", "h-3 w-3")</button>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
@@ -297,20 +288,19 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
|
||||
@keydown="onTagKeydown($event)"
|
||||
@blur="hideEditorTagDropdown()"
|
||||
placeholder="Add tag..."
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
class="input" />
|
||||
<div
|
||||
x-show="showTagDropdown"
|
||||
x-transition
|
||||
class="mt-1 w-full rounded-lg shadow-lg border max-h-48 overflow-y-auto"
|
||||
style="background-color: var(--bg-primary); border-color: var(--border);"
|
||||
x-cloak
|
||||
class="mt-1 w-full rounded-lg border max-h-48 overflow-y-auto"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); box-shadow: var(--shadow-card);"
|
||||
>
|
||||
<template x-for="sug in tagSuggestions" :key="sug.value">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 text-sm flex justify-between items-center"
|
||||
:class="highlightedTagIndex === tagSuggestions.indexOf(sug) ? 'opacity-80' : ''"
|
||||
:style="highlightedTagIndex === tagSuggestions.indexOf(sug) ? 'background-color: var(--bg-secondary); color: var(--text-primary);' : 'color: var(--text-primary);'"
|
||||
class="w-full text-left px-3 py-2 text-sm flex justify-between items-center hover:bg-surface-hover"
|
||||
:class="highlightedTagIndex === tagSuggestions.indexOf(sug) ? 'bg-surface-hover' : ''"
|
||||
@click="selectEditorTagSuggestion(sug.value)"
|
||||
>
|
||||
<span x-text="sug.value"></span>
|
||||
@@ -321,164 +311,142 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Community Rating (0-10)</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Community Rating (0-10)</label>
|
||||
<input type="number" name="community_rating" min="0" max="10" step="0.1"
|
||||
value={ fmt.Sprintf("%.1f", book.CommunityRating.Float64) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
class="input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Publication -->
|
||||
<div class="border rounded-lg" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
||||
style="color: var(--text-primary);"
|
||||
@click="toggleSection('publication')">
|
||||
<span class="font-semibold">Publication</span>
|
||||
<span x-text="openSections.publication ? '▾' : '▸'">▸</span>
|
||||
<span class="inline-flex">
|
||||
<span x-show="openSections.publication">@Icon("chevron-down", "h-4 w-4")</span>
|
||||
<span x-show="!openSections.publication" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
||||
</span>
|
||||
</button>
|
||||
<div x-show="openSections.publication" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
||||
<div x-show="openSections.publication" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Publisher</label>
|
||||
<input type="text" name="publisher" value={ textToString(book.Publisher) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Publisher</label>
|
||||
<input type="text" name="publisher" value={ textToString(book.Publisher) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Date Published</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Date Published</label>
|
||||
<input type="date" name="date_published"
|
||||
value={ formatDateForInput(book.DatePublished) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Edition</label>
|
||||
<input type="text" name="edition" value={ textToString(book.Edition) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Edition</label>
|
||||
<input type="text" name="edition" value={ textToString(book.Edition) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Language</label>
|
||||
<input type="text" name="language" value={ textToString(book.Language) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Language</label>
|
||||
<input type="text" name="language" value={ textToString(book.Language) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Genre</label>
|
||||
<input type="text" name="genre" value={ textToString(book.Genre) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Genre</label>
|
||||
<input type="text" name="genre" value={ textToString(book.Genre) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Copyright Year</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Copyright Year</label>
|
||||
<input type="number" name="copyright_year"
|
||||
value={ fmt.Sprintf("%d", book.CopyrightYear.Int32) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
class="input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Series -->
|
||||
<div class="border rounded-lg" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
||||
style="color: var(--text-primary);"
|
||||
@click="toggleSection('series')">
|
||||
<span class="font-semibold">Series</span>
|
||||
<span x-text="openSections.series ? '▾' : '▸'">▸</span>
|
||||
<span class="inline-flex">
|
||||
<span x-show="openSections.series">@Icon("chevron-down", "h-4 w-4")</span>
|
||||
<span x-show="!openSections.series" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
||||
</span>
|
||||
</button>
|
||||
<div x-show="openSections.series" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
||||
<div x-show="openSections.series" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Series</label>
|
||||
<input type="text" name="series" value={ textToString(book.Series) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Series</label>
|
||||
<input type="text" name="series" value={ textToString(book.Series) } class="input" />
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Number</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Number</label>
|
||||
<input type="number" name="series_number"
|
||||
value={ fmt.Sprintf("%d", book.SeriesNumber.Int32) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Count</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Count</label>
|
||||
<input type="number" name="series_count"
|
||||
value={ fmt.Sprintf("%d", book.SeriesCount.Int32) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Volume</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Volume</label>
|
||||
<input type="number" name="volume"
|
||||
value={ fmt.Sprintf("%d", book.Volume.Int32) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
class="input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Identifiers -->
|
||||
<div class="border rounded-lg" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
||||
style="color: var(--text-primary);"
|
||||
@click="toggleSection('identifiers')">
|
||||
<span class="font-semibold">Identifiers</span>
|
||||
<span x-text="openSections.identifiers ? '▾' : '▸'">▸</span>
|
||||
<span class="inline-flex">
|
||||
<span x-show="openSections.identifiers">@Icon("chevron-down", "h-4 w-4")</span>
|
||||
<span x-show="!openSections.identifiers" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
||||
</span>
|
||||
</button>
|
||||
<div x-show="openSections.identifiers" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
||||
<div x-show="openSections.identifiers" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">ISBN</label>
|
||||
<input type="text" name="isbn" value={ textToString(book.Isbn) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">ISBN</label>
|
||||
<input type="text" name="isbn" value={ textToString(book.Isbn) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">ASIN</label>
|
||||
<input type="text" name="asin" value={ textToString(book.Asin) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">ASIN</label>
|
||||
<input type="text" name="asin" value={ textToString(book.Asin) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Goodreads ID</label>
|
||||
<input type="text" name="goodreads_id" value={ textToString(book.GoodreadsID) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Goodreads ID</label>
|
||||
<input type="text" name="goodreads_id" value={ textToString(book.GoodreadsID) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">OpenLibrary ID</label>
|
||||
<input type="text" name="openlibrary_id" value={ textToString(book.OpenlibraryID) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">OpenLibrary ID</label>
|
||||
<input type="text" name="openlibrary_id" value={ textToString(book.OpenlibraryID) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Google Books ID</label>
|
||||
<input type="text" name="google_books_id" value={ textToString(book.GoogleBooksID) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Google Books ID</label>
|
||||
<input type="text" name="google_books_id" value={ textToString(book.GoogleBooksID) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Web URL</label>
|
||||
<input type="url" name="web_url" value={ textToString(book.WebUrl) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Web URL</label>
|
||||
<input type="url" name="web_url" value={ textToString(book.WebUrl) } class="input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Comic/Manga -->
|
||||
<div class="border rounded-lg" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
||||
style="color: var(--text-primary);"
|
||||
@click="toggleSection('comic')">
|
||||
<span class="font-semibold">Comic/Manga</span>
|
||||
<span x-text="openSections.comic ? '▾' : '▸'">▸</span>
|
||||
<span class="inline-flex">
|
||||
<span x-show="openSections.comic">@Icon("chevron-down", "h-4 w-4")</span>
|
||||
<span x-show="!openSections.comic" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
||||
</span>
|
||||
</button>
|
||||
<div x-show="openSections.comic" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
||||
<div x-show="openSections.comic" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
||||
<div>
|
||||
<label for="manga_type" class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Manga Type</label>
|
||||
<select id="manga_type" name="manga_type"
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);">
|
||||
<label for="manga_type" class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Manga Type</label>
|
||||
<select id="manga_type" name="manga_type" class="input">
|
||||
<option value="unknown" selected={ textToString(book.MangaType) == "unknown" }>Unknown</option>
|
||||
<option value="no" selected={ textToString(book.MangaType) == "no" }>No</option>
|
||||
<option value="yes" selected={ textToString(book.MangaType) == "yes" }>Yes</option>
|
||||
@@ -486,10 +454,8 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="reading_direction" class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Reading Direction</label>
|
||||
<select id="reading_direction" name="reading_direction"
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);">
|
||||
<label for="reading_direction" class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Reading Direction</label>
|
||||
<select id="reading_direction" name="reading_direction" class="input">
|
||||
<option value="auto" selected={ textToString(book.ReadingDirection) == "auto" }>Auto</option>
|
||||
<option value="ltr" selected={ textToString(book.ReadingDirection) == "ltr" }>Left to Right</option>
|
||||
<option value="rtl" selected={ textToString(book.ReadingDirection) == "rtl" }>Right to Left</option>
|
||||
@@ -497,34 +463,24 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Age Rating</label>
|
||||
<input type="text" name="age_rating" value={ textToString(book.AgeRating) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Age Rating</label>
|
||||
<input type="text" name="age_rating" value={ textToString(book.AgeRating) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Story Arc</label>
|
||||
<input type="text" name="story_arc" value={ textToString(book.StoryArc) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Story Arc</label>
|
||||
<input type="text" name="story_arc" value={ textToString(book.StoryArc) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Imprint</label>
|
||||
<input type="text" name="imprint" value={ textToString(book.Imprint) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Imprint</label>
|
||||
<input type="text" name="imprint" value={ textToString(book.Imprint) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Scan Information</label>
|
||||
<input type="text" name="scan_information" value={ textToString(book.ScanInformation) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Scan Information</label>
|
||||
<input type="text" name="scan_information" value={ textToString(book.ScanInformation) } class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Metadata Notes</label>
|
||||
<textarea name="metadata_notes" rows="2"
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);"
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Metadata Notes</label>
|
||||
<textarea name="metadata_notes" rows="2" class="input"
|
||||
>{ textToString(book.MetadataNotes) }</textarea>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -535,40 +491,37 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Technical -->
|
||||
<div class="border rounded-lg" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
|
||||
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
|
||||
style="color: var(--text-primary);"
|
||||
@click="toggleSection('technical')">
|
||||
<span class="font-semibold">Technical</span>
|
||||
<span x-text="openSections.technical ? '▾' : '▸'">▸</span>
|
||||
<span class="inline-flex">
|
||||
<span x-show="openSections.technical">@Icon("chevron-down", "h-4 w-4")</span>
|
||||
<span x-show="!openSections.technical" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
|
||||
</span>
|
||||
</button>
|
||||
<div x-show="openSections.technical" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);">
|
||||
<div x-show="openSections.technical" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Page Count</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Page Count</label>
|
||||
<input type="number" name="page_count"
|
||||
value={ fmt.Sprintf("%d", book.PageCount.Int32) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Contributors (comma-separated)</label>
|
||||
<input type="text" name="contributors" value={ stringSliceToString(book.Contributors) }
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Contributors (comma-separated)</label>
|
||||
<input type="text" name="contributors" value={ stringSliceToString(book.Contributors) } class="input" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Format</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Format</label>
|
||||
<input type="text" value={ textToString(book.MimeType) } readonly
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm opacity-60"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
class="input opacity-60" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">File Size</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">File Size</label>
|
||||
<input type="text" value={ fmt.Sprintf("%.1f MB", float64(book.FileSize.Int64)/1024/1024) } readonly
|
||||
class="w-full px-3 py-2 rounded-lg border text-sm opacity-60"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
|
||||
class="input opacity-60" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -578,16 +531,15 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
|
||||
<div class="flex justify-end space-x-3 p-6 border-t flex-shrink-0" style="border-color: var(--border);">
|
||||
<button
|
||||
@click="hideMetadataEditor()"
|
||||
class="px-4 py-2 rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveMetadata()"
|
||||
class="px-6 py-2 rounded-lg font-semibold"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
@Icon("save", "h-4 w-4")
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+619
-552
File diff suppressed because it is too large
Load Diff
+250
-348
@@ -3,6 +3,7 @@ package templates
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/handlers"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
templ BookShelf(
|
||||
@@ -11,10 +12,10 @@ templ BookShelf(
|
||||
currentLibraryID string,
|
||||
errorMessage string,
|
||||
savedFilters []database.SavedFilters,
|
||||
books []handlers.BookInfo, // NEW
|
||||
limit int, // NEW
|
||||
offset int, // NEW
|
||||
count int, // NEW
|
||||
books []handlers.BookInfo,
|
||||
limit int,
|
||||
offset int,
|
||||
count int,
|
||||
) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -31,363 +32,287 @@ templ BookShelf(
|
||||
x-init="initBookshelf()"
|
||||
>
|
||||
@Header(user, "/bookshelf")
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Filter Bar -->
|
||||
<div
|
||||
class="mb-6 card p-4 rounded-lg border"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||
>
|
||||
<form id="filter-form" hx-get="/api/media-items/search" hx-target="#books-grid">
|
||||
<div class="flex flex-wrap gap-4 items-center">
|
||||
<!-- Library Selector -->
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Library
|
||||
</label>
|
||||
<select
|
||||
id="library-select"
|
||||
name="library_id"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
>
|
||||
if len(libraries) == 0 {
|
||||
<option value="">No libraries available</option>
|
||||
} else {
|
||||
if currentLibraryID == "" {
|
||||
<option value="" selected>All Books ({ TotalMediaCount(libraries) })</option>
|
||||
} else {
|
||||
<option value="">All Books ({ TotalMediaCount(libraries) })</option>
|
||||
}
|
||||
for _, lib := range libraries {
|
||||
if lib.ID == currentLibraryID {
|
||||
<option value={ lib.ID } selected>{ lib.Name } ({ lib.MediaCount })</option>
|
||||
} else {
|
||||
<option value={ lib.ID }>{ lib.Name } ({ lib.MediaCount })</option>
|
||||
}
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-6">
|
||||
<form id="filter-form" hx-get="/api/media-items/search" hx-target="#books-grid">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex flex-wrap items-center gap-3 mb-6">
|
||||
<div class="relative flex-1 min-w-[200px]">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" style="color: var(--text-secondary);">
|
||||
@Icon("search", "h-4 w-4")
|
||||
</span>
|
||||
<input
|
||||
name="q"
|
||||
type="text"
|
||||
class="input pl-10"
|
||||
placeholder="Search all fields…"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
name="sort"
|
||||
class="input w-auto cursor-pointer"
|
||||
hx-get="/api/media-items/search"
|
||||
hx-target="#books-grid"
|
||||
hx-trigger="change"
|
||||
hx-include="#filter-form"
|
||||
>
|
||||
<option value="title ASC">Title (A-Z)</option>
|
||||
<option value="title DESC">Title (Z-A)</option>
|
||||
<option value="author ASC">Author (A-Z)</option>
|
||||
<option value="created_at DESC">Date Added</option>
|
||||
<option value="page_count DESC">Page Count</option>
|
||||
</select>
|
||||
<button type="button" @click="filtersOpen = true" class="btn btn-secondary">
|
||||
@Icon("filter", "h-4 w-4")
|
||||
<span>Filters</span>
|
||||
</button>
|
||||
<button type="button" @click="showSaveFilterModal()" class="btn btn-secondary">
|
||||
@Icon("save", "h-4 w-4")
|
||||
<span>Save</span>
|
||||
</button>
|
||||
<div class="relative">
|
||||
<button type="button" @click="toggleFiltersDropdown()" data-load-filter-btn class="btn btn-secondary">
|
||||
@Icon("folder", "h-4 w-4")
|
||||
<span>Load</span>
|
||||
</button>
|
||||
<div
|
||||
x-show="showFiltersDropdown"
|
||||
@click.outside="showFiltersDropdown = false"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute top-full right-0 mt-2 w-80 max-w-[calc(100vw-2rem)] rounded-xl shadow-lg z-50 card p-4"
|
||||
x-cloak
|
||||
>
|
||||
<h3 class="text-sm font-semibold mb-3" style="color: var(--text-primary)">Saved Filters</h3>
|
||||
<div class="space-y-2" id="saved-filters-list">
|
||||
for _, filter := range savedFilters {
|
||||
<div class="flex items-center justify-between p-2 rounded-lg hover:bg-surface-hover" data-filter-id={ uuidToString(filter.ID) }>
|
||||
<button data-action="load-filter" @click="loadFilter($event)" class="flex-1 text-left px-2 py-1 rounded text-sm" style="color: var(--text-primary);">
|
||||
{ filter.Name }
|
||||
</button>
|
||||
<button data-action="delete-filter" @click="deleteFilter($event)" class="icon-btn h-8 w-8" title="Delete filter">
|
||||
@Icon("trash", "h-4 w-4")
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
if len(savedFilters) == 0 {
|
||||
<div class="text-sm py-4 text-center" style="color: var(--text-secondary);">
|
||||
No saved filters yet
|
||||
</div>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<!-- Search Input -->
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Search
|
||||
</label>
|
||||
<input
|
||||
name="q"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
placeholder="Search all fields..."
|
||||
/>
|
||||
</div>
|
||||
<button type="button" @click="clearFilters()" class="btn btn-ghost">
|
||||
@Icon("close", "h-4 w-4")
|
||||
<span>Clear</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Filter drawer -->
|
||||
<div
|
||||
x-show="filtersOpen"
|
||||
x-cloak
|
||||
class="fixed inset-0 z-[65]"
|
||||
@keydown.escape.window="filtersOpen = false"
|
||||
>
|
||||
<div class="absolute inset-0" style="background-color: var(--surface-overlay);" @click="filtersOpen = false"></div>
|
||||
<div
|
||||
class="absolute right-0 top-0 bottom-0 w-full max-w-md overflow-y-auto"
|
||||
style="background-color: var(--bg-secondary); box-shadow: var(--shadow-pop);"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="translate-x-full"
|
||||
x-transition:enter-end="translate-x-0"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="translate-x-0"
|
||||
x-transition:leave-end="translate-x-full"
|
||||
>
|
||||
<div class="sticky top-0 flex items-center justify-between px-6 py-4 border-b" style="background-color: var(--bg-secondary); border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<h2 class="text-lg font-bold" style="color: var(--text-primary)">Filters</h2>
|
||||
<button type="button" @click="filtersOpen = false" class="icon-btn" aria-label="Close filters">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<!-- Author Filter with Autocomplete -->
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Author
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="author_filter"
|
||||
placeholder="Filter by author"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
list="author-datalist"
|
||||
@input.debounce.300ms="if($el.value.length >= 2) fetchAuthorValues($el)"
|
||||
/>
|
||||
<datalist id="author-datalist"></datalist>
|
||||
</div>
|
||||
<!-- Tags Filter with Autocomplete -->
|
||||
<div class="flex-1 min-w-[150px] relative">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Tags</label>
|
||||
<input
|
||||
type="text"
|
||||
name="tags_filter"
|
||||
placeholder="Filter by tags"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
@input.debounce.300ms="if($el.value.length >= 2) fetchTagValues($el)"
|
||||
@keydown="onTagFilterKeydown($event)"
|
||||
@blur="hideTagDropdown()"
|
||||
/>
|
||||
<div
|
||||
x-show="showTagDropdown"
|
||||
x-transition
|
||||
class="absolute z-50 mt-1 w-full rounded-lg shadow-lg border max-h-48 overflow-y-auto"
|
||||
style="background-color: var(--bg-primary); border-color: var(--border);"
|
||||
>
|
||||
<template x-for="sug in tagSuggestions" :key="sug.value">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 text-sm flex justify-between items-center"
|
||||
:class="tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'opacity-80' : ''"
|
||||
:style="tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'background-color: var(--bg-secondary); color: var(--text-primary);' : 'color: var(--text-primary);'"
|
||||
@click="selectTagSuggestion(sug.value)"
|
||||
>
|
||||
<span x-text="sug.value"></span>
|
||||
<span class="text-xs" style="color: var(--text-secondary);" x-text="sug.count + ' books'"></span>
|
||||
</button>
|
||||
<div class="p-6 space-y-5">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library</label>
|
||||
<select id="library-select" name="library_id" class="input">
|
||||
if len(libraries) == 0 {
|
||||
<option value="">No libraries available</option>
|
||||
} else {
|
||||
if currentLibraryID == "" {
|
||||
<option value="" selected>All Books ({ TotalMediaCount(libraries) })</option>
|
||||
} else {
|
||||
<option value="">All Books ({ TotalMediaCount(libraries) })</option>
|
||||
}
|
||||
for _, lib := range libraries {
|
||||
if lib.ID == currentLibraryID {
|
||||
<option value={ lib.ID } selected>{ lib.Name } ({ lib.MediaCount })</option>
|
||||
} else {
|
||||
<option value={ lib.ID }>{ lib.Name } ({ lib.MediaCount })</option>
|
||||
}
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Author</label>
|
||||
<input
|
||||
type="text"
|
||||
name="author_filter"
|
||||
placeholder="Filter by author"
|
||||
class="input"
|
||||
list="author-datalist"
|
||||
@input.debounce.300ms="if($el.value.length >= 2) fetchAuthorValues($el)"
|
||||
/>
|
||||
<datalist id="author-datalist"></datalist>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Tags</label>
|
||||
<input
|
||||
type="text"
|
||||
name="tags_filter"
|
||||
placeholder="Filter by tags"
|
||||
class="input"
|
||||
@input.debounce.300ms="if($el.value.length >= 2) fetchTagValues($el)"
|
||||
@keydown="onTagFilterKeydown($event)"
|
||||
@blur="hideTagDropdown()"
|
||||
/>
|
||||
<div
|
||||
x-show="showTagDropdown"
|
||||
x-transition
|
||||
x-cloak
|
||||
class="absolute z-50 mt-1 w-full rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
||||
style="background-color: var(--bg-primary); border: 1px solid var(--border);"
|
||||
>
|
||||
<template x-for="sug in tagSuggestions" :key="sug.value">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 text-sm flex justify-between items-center"
|
||||
:class="tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'opacity-80' : ''"
|
||||
:style="tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'background-color: var(--bg-secondary); color: var(--text-primary);' : 'color: var(--text-primary);'"
|
||||
@click="selectTagSuggestion(sug.value)"
|
||||
>
|
||||
<span x-text="sug.value"></span>
|
||||
<span class="text-xs" style="color: var(--text-secondary);" x-text="sug.count + ' books'"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Series</label>
|
||||
<input
|
||||
type="text"
|
||||
name="series_filter"
|
||||
placeholder="Filter by series"
|
||||
class="input"
|
||||
list="series-datalist"
|
||||
@input.debounce.300ms="if($el.value.length >= 2) fetchSeriesValues($el)"
|
||||
/>
|
||||
<datalist id="series-datalist"></datalist>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Language</label>
|
||||
<input
|
||||
type="text"
|
||||
name="language_filter"
|
||||
placeholder="Filter by language"
|
||||
class="input"
|
||||
list="language-datalist"
|
||||
@input.debounce.300ms="if($el.value.length >= 2) fetchLanguageValues($el)"
|
||||
/>
|
||||
<datalist id="language-datalist"></datalist>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Year Range</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="number" name="year_min" placeholder="From" class="input"/>
|
||||
<input type="number" name="year_max" placeholder="To" class="input"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Cover</label>
|
||||
<input
|
||||
type="button"
|
||||
id="has-cover-tristate"
|
||||
class="tristate-btn w-full transition-all duration-300 cursor-pointer flex items-center justify-center gap-2 px-4 py-2 rounded-lg border text-sm"
|
||||
:class="{
|
||||
'state-null': hasCoverState === null,
|
||||
'state-true': hasCoverState === true,
|
||||
'state-false': hasCoverState === false
|
||||
}"
|
||||
:value="hasCoverState === null ? '○ Any cover' : hasCoverState === true ? '✓ Has cover' : '✗ No cover'"
|
||||
@click="cycleHasCover()"
|
||||
/>
|
||||
<template x-if="hasCoverState !== null">
|
||||
<input
|
||||
type="hidden"
|
||||
name="has_cover"
|
||||
:value="hasCoverState ? 'true' : 'false'"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
// <!-- Genre Filter with Autocomplete -->
|
||||
// <div class="flex-1 min-w-[150px]">
|
||||
// <label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
// Genre
|
||||
// </label>
|
||||
// <input
|
||||
// type="text"
|
||||
// name="genre_filter"
|
||||
// placeholder="Filter by genre"
|
||||
// class="w-full px-3 py-2 border rounded-lg"
|
||||
// style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
// list="genre-datalist"
|
||||
// @input.debounce.300ms="if($el.value.length >= 2) fetchGenreValues($el)"
|
||||
// />
|
||||
// <datalist id="genre-datalist"></datalist>
|
||||
// </div>
|
||||
<!-- Series Filter with Autocomplete (NEW) -->
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Series
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="series_filter"
|
||||
placeholder="Filter by series"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
list="series-datalist"
|
||||
@input.debounce.300ms="if($el.value.length >= 2) fetchSeriesValues($el)"
|
||||
/>
|
||||
<datalist id="series-datalist"></datalist>
|
||||
</div>
|
||||
<!-- Language Filter with Autocomplete (NEW) -->
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Language
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="language_filter"
|
||||
placeholder="Filter by language"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
list="language-datalist"
|
||||
@input.debounce.300ms="if($el.value.length >= 2) fetchLanguageValues($el)"
|
||||
/>
|
||||
<datalist id="language-datalist"></datalist>
|
||||
</div>
|
||||
<!-- Year Range -->
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Year Range
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="number"
|
||||
name="year_min"
|
||||
placeholder="From"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
name="year_max"
|
||||
placeholder="To"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Has Cover Filter - TriState Button -->
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Cover Filter
|
||||
</label>
|
||||
<input
|
||||
type="button"
|
||||
id="has-cover-tristate"
|
||||
class="tristate-btn w-full transition-all duration-300 cursor-pointer flex items-center justify-center gap-2 px-4 py-2 rounded-lg border text-sm"
|
||||
:class="{
|
||||
'state-null': hasCoverState === null,
|
||||
'state-true': hasCoverState === true,
|
||||
'state-false': hasCoverState === false
|
||||
}"
|
||||
:value="hasCoverState === null ? '○ Cover: Any' : hasCoverState === true ? '✓ Has Cover' : '✗ No Cover'"
|
||||
@click="cycleHasCover()"
|
||||
/>
|
||||
<!-- Hidden input for HTMX form submission - conditionally rendered -->
|
||||
<template x-if="hasCoverState !== null">
|
||||
<input
|
||||
type="hidden"
|
||||
name="has_cover"
|
||||
:value="hasCoverState ? 'true' : 'false'"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Sort By Dropdown (PRESERVED) -->
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Sort By
|
||||
</label>
|
||||
<select
|
||||
name="sort"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
hx-get="/api/media-items/search"
|
||||
hx-target="#books-grid"
|
||||
hx-trigger="change"
|
||||
hx-include="#filter-form"
|
||||
>
|
||||
<option value="title ASC">Title (A-Z)</option>
|
||||
<option value="title DESC">Title (Z-A)</option>
|
||||
<option value="author ASC">Author (A-Z)</option>
|
||||
<option value="created_at DESC">Date Added</option>
|
||||
<option value="page_count DESC">Page Count</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Submit Button -->
|
||||
<div class="flex items-end">
|
||||
<button
|
||||
type="submit"
|
||||
class="px-6 py-2 rounded-lg font-medium"
|
||||
style="background-color: var(--accent); color: var(--bg-primary);"
|
||||
>
|
||||
🔍 Search
|
||||
</button>
|
||||
</div>
|
||||
<!-- Save Filter Button (PRESERVED) -->
|
||||
<div class="flex items-end">
|
||||
<button
|
||||
@click="showSaveFilterModal()"
|
||||
class="px-4 py-2 rounded-lg font-medium"
|
||||
style="background-color: var(--accent); color: var(--bg-primary);"
|
||||
>
|
||||
💾 Save Filter
|
||||
</button>
|
||||
</div>
|
||||
<!-- Load Filter Button (PRESERVED) -->
|
||||
<div class="flex items-end relative">
|
||||
<button
|
||||
@click="toggleFiltersDropdown()"
|
||||
data-load-filter-btn
|
||||
class="px-4 py-2 rounded-lg font-medium border"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
>
|
||||
📂 Load Filter
|
||||
</button>
|
||||
<!-- Saved Filters Dropdown (PRESERVED) -->
|
||||
<div
|
||||
x-show="showFiltersDropdown"
|
||||
@click.outside="showFiltersDropdown = false"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute top-full mt-2 w-80 rounded-lg shadow-lg z-50"
|
||||
:class="dropdownAlign === 'left' ? 'left-0' : 'right-0'"
|
||||
style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;"
|
||||
>
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-semibold mb-3" style="color: var(--text-primary)">
|
||||
Saved Filters
|
||||
</h3>
|
||||
<!-- Filter List -->
|
||||
<div class="space-y-2" id="saved-filters-list">
|
||||
for _, filter := range savedFilters {
|
||||
<div class="flex items-center justify-between p-2 rounded hover:opacity-80" style="background-color: var(--bg-primary);" data-filter-id={ uuidToString(filter.ID) }>
|
||||
<button data-action="load-filter" @click="loadFilter($event)" class="flex-1 text-left px-2 py-1 rounded" style="color: var(--text-primary);">
|
||||
{ filter.Name }
|
||||
</button>
|
||||
<button data-action="delete-filter" @click="deleteFilter($event)" class="p-1 hover:opacity-70 rounded" style="color: var(--text-secondary);" title="Delete filter">🗑️</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- Empty State -->
|
||||
if len(savedFilters) == 0 {
|
||||
<div class="text-sm py-4 text-center" style="color: var(--text-secondary);">
|
||||
No saved filters yet
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Clear Filters Button (PRESERVED) -->
|
||||
<div class="flex items-end">
|
||||
<button
|
||||
@click="clearFilters()"
|
||||
class="px-4 py-2 rounded-lg font-medium border"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
>
|
||||
✕ Clear
|
||||
</button>
|
||||
<div class="sticky bottom-0 px-6 py-4 border-t" style="background-color: var(--bg-secondary); border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<button type="submit" @click="filtersOpen = false" class="btn btn-primary w-full">Apply Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Books Grid -->
|
||||
<div id="books-grid" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
|
||||
</div>
|
||||
</form>
|
||||
<div id="books-grid" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
@BooksGrid(books, limit, offset, count, currentLibraryID)
|
||||
<!-- Error Message -->
|
||||
if errorMessage != "" {
|
||||
<div class="mt-6 p-4 rounded-lg border bg-red-500/10 border-red-500">
|
||||
<div class="col-span-full mt-2 p-4 rounded-xl border" style="background-color: color-mix(in srgb, var(--status-danger) 10%, transparent); border-color: var(--status-danger);">
|
||||
<p style="color: var(--text-primary)">{ errorMessage }</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div id="pagination" class="mt-6 flex justify-center gap-2">
|
||||
<div id="pagination" class="mt-6 flex justify-center items-center gap-2">
|
||||
if count > 0 {
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 rounded-lg border disabled:opacity-50"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
hx-get="/api/media-items/search?library_id={ currentLibraryID }&limit={ limit }&offset={ offset - limit }"
|
||||
class="btn btn-secondary disabled:opacity-40"
|
||||
hx-get={ fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset-limit) }
|
||||
hx-target="#books-grid"
|
||||
hx-include="#filter-form"
|
||||
disabled?={ offset <= 0 }
|
||||
>
|
||||
← Previous
|
||||
@Icon("chevron-left", "h-4 w-4")
|
||||
<span>Prev</span>
|
||||
</button>
|
||||
<span class="px-4 py-2" style="color: var(--text-secondary);">
|
||||
<span class="px-4 py-2 text-sm" style="color: var(--text-secondary);">
|
||||
Page { offset / limit + 1 }
|
||||
</span>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg border disabled:opacity-50"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
hx-get="/api/media-items/search?library_id={ currentLibraryID }&limit={ limit }&offset={ offset + limit }"
|
||||
type="button"
|
||||
class="btn btn-secondary disabled:opacity-40"
|
||||
hx-get={ fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset+limit) }
|
||||
hx-target="#books-grid"
|
||||
hx-include="#filter-form"
|
||||
disabled?={ offset+limit >= count }
|
||||
>
|
||||
Next →
|
||||
<span>Next</span>
|
||||
@Icon("chevron-right", "h-4 w-4")
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Save Filter Modal -->
|
||||
<div
|
||||
x-show="showSaveModal"
|
||||
@click.self="showSaveModal = false"
|
||||
x-transition
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="background-color: rgba(0, 0, 0, 0.7); display: none;"
|
||||
x-cloak
|
||||
class="fixed inset-0 z-[70] flex items-center justify-center p-4"
|
||||
style="background-color: var(--surface-overlay);"
|
||||
>
|
||||
<div
|
||||
@click.stop
|
||||
class="card rounded-lg p-6 w-full max-w-md mx-4"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||
>
|
||||
<div @click.stop class="card rounded-2xl p-6 w-full max-w-md">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Save Filter</h2>
|
||||
<button
|
||||
@click="showSaveModal = false"
|
||||
class="p-2 hover:opacity-80 rounded"
|
||||
style="color: var(--text-primary)"
|
||||
>✕</button>
|
||||
<button @click="showSaveModal = false" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<form
|
||||
hx-post="/api/saved-filters"
|
||||
@@ -397,38 +322,15 @@ templ BookShelf(
|
||||
@htmx:afterRequest="if(event.detail.xhr.status < 400) { afterFilterSave() }"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Filter Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="filter_name"
|
||||
placeholder="My Custom Filter"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
required
|
||||
/>
|
||||
<div id="filter-save-error" style="display: none; color: #ef4444; padding: 0.75rem; border-radius: 0.5rem; margin-bottom: 1rem;"></div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Filter Name</label>
|
||||
<input type="text" name="filter_name" placeholder="My Custom Filter" class="input" required/>
|
||||
<div id="filter-save-error" style="display: none; color: var(--status-danger); padding: 0.75rem; border-radius: 0.5rem; margin-top: 0.75rem;"></div>
|
||||
<input type="hidden" name="resource_type" value="media-items"/>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="showSaveModal = false"
|
||||
class="px-4 py-2 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 rounded-lg font-medium"
|
||||
style="background-color: var(--accent); color: var(--bg-primary);"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button type="button" @click="showSaveModal = false" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
<!-- Hidden pagination state -->
|
||||
<input type="hidden" name="limit" value="50"/>
|
||||
<input type="hidden" name="offset" value="0"/>
|
||||
</form>
|
||||
|
||||
+242
-139
File diff suppressed because one or more lines are too long
@@ -16,20 +16,21 @@ templ BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, backUrl)
|
||||
<div class="sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b" style="background-color: var(--bg-primary);">
|
||||
<div class="w-full px-4 py-3 flex items-center gap-4">
|
||||
<a href={ backUrl } class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
← { backLabel }
|
||||
<div class="app-subbar border-b" style="background-color: color-mix(in srgb, var(--bg-secondary) 82%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-color: color-mix(in srgb, var(--text-primary) 6%, transparent);">
|
||||
<div class="mx-auto container px-4 sm:px-6 lg:px-8 py-3">
|
||||
<a href={ backUrl } class="btn btn-ghost">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
<span>{ backLabel }</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full px-4 py-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<span class="px-3 py-1 rounded-full text-sm font-semibold" style="background-color: var(--accent); color: white;">
|
||||
<div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flex flex-wrap items-center gap-3 mb-6">
|
||||
<span class="chip" style="background-color: var(--accent); color: var(--bg-primary);">
|
||||
{ badgeIcon } { badgeLabel }
|
||||
</span>
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ title }</h1>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">{ fmt.Sprintf("%d", len(books)) } books</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">{ title }</h1>
|
||||
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">{ fmt.Sprintf("%d", len(books)) } books</span>
|
||||
</div>
|
||||
if len(books) > 0 {
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
|
||||
@@ -38,10 +39,10 @@ templ BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<div class="text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">{ emptyIcon }</div>
|
||||
<div class="card text-center py-16 px-6">
|
||||
<div class="text-5xl mb-4">{ emptyIcon }</div>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Books Found</h3>
|
||||
<p>{ emptyMessage }</p>
|
||||
<p style="color: var(--text-secondary)">{ emptyMessage }</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b\" style=\"background-color: var(--bg-primary);\"><div class=\"w-full px-4 py-3 flex items-center gap-4\"><a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"app-subbar border-b\" style=\"background-color: color-mix(in srgb, var(--bg-secondary) 82%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-color: color-mix(in srgb, var(--text-primary) 6%, transparent);\"><div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-3\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -68,77 +68,85 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"text-sm hover:opacity-80 transition-opacity\" style=\"color: var(--text-secondary); text-decoration: none;\">← ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"btn btn-ghost\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(backLabel)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 22, Col: 21}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 23, Col: 23}
|
||||
}
|
||||
_, 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, 5, "</a></div></div><div class=\"w-full px-4 py-8\"><div class=\"flex items-center gap-4 mb-6\"><span class=\"px-3 py-1 rounded-full text-sm font-semibold\" style=\"background-color: var(--accent); color: white;\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span></a></div></div><div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"flex flex-wrap items-center gap-3 mb-6\"><span class=\"chip\" style=\"background-color: var(--accent); color: var(--bg-primary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(badgeIcon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 29, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 30, Col: 17}
|
||||
}
|
||||
_, 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, " ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(badgeLabel)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 29, Col: 32}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 30, Col: 32}
|
||||
}
|
||||
_, 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, "</span><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 31, Col: 78}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 32, Col: 93}
|
||||
}
|
||||
_, 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, 8, "</h1><span class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h1><span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(books)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 32, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 33, Col: 125}
|
||||
}
|
||||
_, 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, 9, " books</span></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " books</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(books) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -148,43 +156,43 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"card text-center py-16 px-6\"><div class=\"text-5xl mb-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(emptyIcon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 42, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 43, Col: 44}
|
||||
}
|
||||
_, 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, 13, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Books Found</h3><p>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Books Found</h3><p style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(emptyMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 44, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 45, Col: 60}
|
||||
}
|
||||
_, 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, 14, "</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -192,7 +200,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package templates
|
||||
|
||||
templ CollectionModal(collection CollectionData) {
|
||||
<div x-data="collections" class="fixed inset-0 z-50 flex items-center justify-center bg-black/70">
|
||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div x-data="collections" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
if collection.ID != "" {
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Collection</h2>
|
||||
} else {
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Collection</h2>
|
||||
}
|
||||
<button type="button" @click="closeCollectionModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("folder", "h-5 w-5")
|
||||
</span>
|
||||
if collection.ID != "" {
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Collection</h2>
|
||||
} else {
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Collection</h2>
|
||||
}
|
||||
</div>
|
||||
<button type="button" @click="closeCollectionModal()" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
if collection.ID != "" {
|
||||
<form
|
||||
@@ -18,65 +25,57 @@ templ CollectionModal(collection CollectionData) {
|
||||
>
|
||||
<input type="hidden" name="id" value={ collection.ID }/>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={ collection.Name }
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
placeholder="My Reading List"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
placeholder="Optional description"
|
||||
rows="3"
|
||||
>{ collection.Description }</textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Icon</label>
|
||||
<!-- Search/Text Input -->
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Icon</label>
|
||||
<input
|
||||
type="text"
|
||||
id="icon-search"
|
||||
class="w-full px-4 py-2 border rounded-lg mb-2"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
placeholder="🔍 Search or type emoji..."
|
||||
class="input mb-2"
|
||||
placeholder="Search or type emoji..."
|
||||
maxlength="4"
|
||||
oninput="filterIcons(this.value)"
|
||||
onfocus="showAllIcons()"
|
||||
@input="filterIcons($event.target.value)"
|
||||
@focus="showAllIcons()"
|
||||
/>
|
||||
<!-- Hidden input for form submission -->
|
||||
<input type="hidden" name="icon" id="collection-icon" value={ collection.Icon }/>
|
||||
<!-- Emoji Grid -->
|
||||
<div
|
||||
id="icon-grid"
|
||||
class="grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 border rounded-lg"
|
||||
class="grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 rounded-lg border"
|
||||
style="background-color: var(--bg-primary); border-color: var(--border);"
|
||||
>
|
||||
<!-- Populated dynamically by JavaScript -->
|
||||
</div>
|
||||
></div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Color</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Color</label>
|
||||
<div class="flex gap-2">
|
||||
<button type="button" @click="selectColor('blue')" class="w-8 h-8 rounded-full color-option bg-blue-500"></button>
|
||||
<button type="button" @click="selectColor('red')" class="w-8 h-8 rounded-full color-option bg-red-500"></button>
|
||||
<button type="button" @click="selectColor('yellow')" class="w-8 h-8 rounded-full color-option bg-yellow-500"></button>
|
||||
<button type="button" @click="selectColor('green')" class="w-8 h-8 rounded-full color-option bg-green-500"></button>
|
||||
<button type="button" @click="selectColor('purple')" class="w-8 h-8 rounded-full color-option bg-purple-500"></button>
|
||||
<button type="button" @click="selectColor('blue')" data-color="blue" class="w-8 h-8 rounded-full color-option bg-blue-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-blue-400" aria-label="Blue"></button>
|
||||
<button type="button" @click="selectColor('red')" data-color="red" class="w-8 h-8 rounded-full color-option bg-red-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-red-400" aria-label="Red"></button>
|
||||
<button type="button" @click="selectColor('yellow')" data-color="yellow" class="w-8 h-8 rounded-full color-option bg-yellow-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-yellow-400" aria-label="Yellow"></button>
|
||||
<button type="button" @click="selectColor('green')" data-color="green" class="w-8 h-8 rounded-full color-option bg-green-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-green-400" aria-label="Green"></button>
|
||||
<button type="button" @click="selectColor('purple')" data-color="purple" class="w-8 h-8 rounded-full color-option bg-purple-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-purple-400" aria-label="Purple"></button>
|
||||
</div>
|
||||
<input type="hidden" name="color" id="collection-color" value={ collection.Color }/>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="closeCollectionModal()" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">Update Collection</button>
|
||||
<button type="button" @click="closeCollectionModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Update Collection</button>
|
||||
</div>
|
||||
</form>
|
||||
} else {
|
||||
@@ -85,64 +84,56 @@ templ CollectionModal(collection CollectionData) {
|
||||
hx-redirect="/collections"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
placeholder="My Reading List"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
placeholder="Optional description"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Icon</label>
|
||||
<!-- Search/Text Input -->
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Icon</label>
|
||||
<input
|
||||
type="text"
|
||||
id="icon-search"
|
||||
class="w-full px-4 py-2 border rounded-lg mb-2"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
placeholder="🔍 Search or type emoji..."
|
||||
class="input mb-2"
|
||||
placeholder="Search or type emoji..."
|
||||
maxlength="4"
|
||||
oninput="filterIcons(this.value)"
|
||||
onfocus="showAllIcons()"
|
||||
@input="filterIcons($event.target.value)"
|
||||
@focus="showAllIcons()"
|
||||
/>
|
||||
<!-- Hidden input for form submission -->
|
||||
<input type="hidden" name="icon" id="collection-icon" value={ collection.Icon }/>
|
||||
<!-- Emoji Grid -->
|
||||
<div
|
||||
id="icon-grid"
|
||||
class="grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 border rounded-lg"
|
||||
class="grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 rounded-lg border"
|
||||
style="background-color: var(--bg-primary); border-color: var(--border);"
|
||||
>
|
||||
<!-- Populated dynamically by JavaScript -->
|
||||
</div>
|
||||
></div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Color</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Color</label>
|
||||
<div class="flex gap-2">
|
||||
<button type="button" @click="selectColor('blue')" class="w-8 h-8 rounded-full color-option bg-blue-500"></button>
|
||||
<button type="button" @click="selectColor('red')" class="w-8 h-8 rounded-full color-option bg-red-500"></button>
|
||||
<button type="button" @click="selectColor('yellow')" class="w-8 h-8 rounded-full color-option bg-yellow-500"></button>
|
||||
<button type="button" @click="selectColor('green')" class="w-8 h-8 rounded-full color-option bg-green-500"></button>
|
||||
<button type="button" @click="selectColor('purple')" class="w-8 h-8 rounded-full color-option bg-purple-500"></button>
|
||||
<button type="button" @click="selectColor('blue')" data-color="blue" class="w-8 h-8 rounded-full color-option bg-blue-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-blue-400" aria-label="Blue"></button>
|
||||
<button type="button" @click="selectColor('red')" data-color="red" class="w-8 h-8 rounded-full color-option bg-red-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-red-400" aria-label="Red"></button>
|
||||
<button type="button" @click="selectColor('yellow')" data-color="yellow" class="w-8 h-8 rounded-full color-option bg-yellow-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-yellow-400" aria-label="Yellow"></button>
|
||||
<button type="button" @click="selectColor('green')" data-color="green" class="w-8 h-8 rounded-full color-option bg-green-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-green-400" aria-label="Green"></button>
|
||||
<button type="button" @click="selectColor('purple')" data-color="purple" class="w-8 h-8 rounded-full color-option bg-purple-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-purple-400" aria-label="Purple"></button>
|
||||
</div>
|
||||
<input type="hidden" name="color" id="collection-color" value="blue"/>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="closeCollectionModal()" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">Create Collection</button>
|
||||
<button type="button" @click="closeCollectionModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create Collection</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@@ -29,128 +29,144 @@ func CollectionModal(collection CollectionData) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"collections\" class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/70\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"collections\" class=\"fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("folder", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if collection.ID != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Edit Collection</h2>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Edit Collection</h2>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Create Collection</h2>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Create Collection</h2>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<button type=\"button\" @click=\"closeCollectionModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><button type=\"button\" @click=\"closeCollectionModal()\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if collection.ID != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<form hx-put=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<form hx-put=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/collections/" + collection.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 16, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 23, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" hx-redirect=\"/collections\"><input type=\"hidden\" name=\"id\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" hx-redirect=\"/collections\"><input type=\"hidden\" name=\"id\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 19, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 26, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Name</label> <input type=\"text\" name=\"name\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Name</label> <input type=\"text\" name=\"name\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 25, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 32, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"My Reading List\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"Optional description\" rows=\"3\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" required class=\"input\" placeholder=\"My Reading List\"></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" class=\"input\" placeholder=\"Optional description\" rows=\"3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 40, Col: 31}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 45, Col: 31}
|
||||
}
|
||||
_, 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, 9, "</textarea></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Icon</label><!-- Search/Text Input --><input type=\"text\" id=\"icon-search\" class=\"w-full px-4 py-2 border rounded-lg mb-2\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"🔍 Search or type emoji...\" maxlength=\"4\" oninput=\"filterIcons(this.value)\" onfocus=\"showAllIcons()\"><!-- Hidden input for form submission --><input type=\"hidden\" name=\"icon\" id=\"collection-icon\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</textarea></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Icon</label> <input type=\"text\" id=\"icon-search\" class=\"input mb-2\" placeholder=\"Search or type emoji...\" maxlength=\"4\" @input=\"filterIcons($event.target.value)\" @focus=\"showAllIcons()\"> <input type=\"hidden\" name=\"icon\" id=\"collection-icon\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 56, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 58, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"><!-- Emoji Grid --><div id=\"icon-grid\" class=\"grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 border rounded-lg\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><!-- Populated dynamically by JavaScript --></div></div><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Color</label><div class=\"flex gap-2\"><button type=\"button\" @click=\"selectColor('blue')\" class=\"w-8 h-8 rounded-full color-option bg-blue-500\"></button> <button type=\"button\" @click=\"selectColor('red')\" class=\"w-8 h-8 rounded-full color-option bg-red-500\"></button> <button type=\"button\" @click=\"selectColor('yellow')\" class=\"w-8 h-8 rounded-full color-option bg-yellow-500\"></button> <button type=\"button\" @click=\"selectColor('green')\" class=\"w-8 h-8 rounded-full color-option bg-green-500\"></button> <button type=\"button\" @click=\"selectColor('purple')\" class=\"w-8 h-8 rounded-full color-option bg-purple-500\"></button></div><input type=\"hidden\" name=\"color\" id=\"collection-color\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"><div id=\"icon-grid\" class=\"grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 rounded-lg border\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"></div></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Color</label><div class=\"flex gap-2\"><button type=\"button\" @click=\"selectColor('blue')\" data-color=\"blue\" class=\"w-8 h-8 rounded-full color-option bg-blue-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-blue-400\" aria-label=\"Blue\"></button> <button type=\"button\" @click=\"selectColor('red')\" data-color=\"red\" class=\"w-8 h-8 rounded-full color-option bg-red-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-red-400\" aria-label=\"Red\"></button> <button type=\"button\" @click=\"selectColor('yellow')\" data-color=\"yellow\" class=\"w-8 h-8 rounded-full color-option bg-yellow-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-yellow-400\" aria-label=\"Yellow\"></button> <button type=\"button\" @click=\"selectColor('green')\" data-color=\"green\" class=\"w-8 h-8 rounded-full color-option bg-green-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-green-400\" aria-label=\"Green\"></button> <button type=\"button\" @click=\"selectColor('purple')\" data-color=\"purple\" class=\"w-8 h-8 rounded-full color-option bg-purple-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-purple-400\" aria-label=\"Purple\"></button></div><input type=\"hidden\" name=\"color\" id=\"collection-color\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Color)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 75, Col: 86}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 74, Col: 86}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Update Collection</button></div></form>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">Update Collection</button></div></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<form hx-post=\"/api/collections\" hx-redirect=\"/collections\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Name</label> <input type=\"text\" name=\"name\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"My Reading List\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"Optional description\" rows=\"3\"></textarea></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Icon</label><!-- Search/Text Input --><input type=\"text\" id=\"icon-search\" class=\"w-full px-4 py-2 border rounded-lg mb-2\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"🔍 Search or type emoji...\" maxlength=\"4\" oninput=\"filterIcons(this.value)\" onfocus=\"showAllIcons()\"><!-- Hidden input for form submission --><input type=\"hidden\" name=\"icon\" id=\"collection-icon\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<form hx-post=\"/api/collections\" hx-redirect=\"/collections\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Name</label> <input type=\"text\" name=\"name\" required class=\"input\" placeholder=\"My Reading List\"></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" class=\"input\" placeholder=\"Optional description\" rows=\"3\"></textarea></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Icon</label> <input type=\"text\" id=\"icon-search\" class=\"input mb-2\" placeholder=\"Search or type emoji...\" maxlength=\"4\" @input=\"filterIcons($event.target.value)\" @focus=\"showAllIcons()\"> <input type=\"hidden\" name=\"icon\" id=\"collection-icon\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 122, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 116, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"><!-- Emoji Grid --><div id=\"icon-grid\" class=\"grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 border rounded-lg\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><!-- Populated dynamically by JavaScript --></div></div><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Color</label><div class=\"flex gap-2\"><button type=\"button\" @click=\"selectColor('blue')\" class=\"w-8 h-8 rounded-full color-option bg-blue-500\"></button> <button type=\"button\" @click=\"selectColor('red')\" class=\"w-8 h-8 rounded-full color-option bg-red-500\"></button> <button type=\"button\" @click=\"selectColor('yellow')\" class=\"w-8 h-8 rounded-full color-option bg-yellow-500\"></button> <button type=\"button\" @click=\"selectColor('green')\" class=\"w-8 h-8 rounded-full color-option bg-green-500\"></button> <button type=\"button\" @click=\"selectColor('purple')\" class=\"w-8 h-8 rounded-full color-option bg-purple-500\"></button></div><input type=\"hidden\" name=\"color\" id=\"collection-color\" value=\"blue\"></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Create Collection</button></div></form>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"><div id=\"icon-grid\" class=\"grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 rounded-lg border\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"></div></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Color</label><div class=\"flex gap-2\"><button type=\"button\" @click=\"selectColor('blue')\" data-color=\"blue\" class=\"w-8 h-8 rounded-full color-option bg-blue-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-blue-400\" aria-label=\"Blue\"></button> <button type=\"button\" @click=\"selectColor('red')\" data-color=\"red\" class=\"w-8 h-8 rounded-full color-option bg-red-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-red-400\" aria-label=\"Red\"></button> <button type=\"button\" @click=\"selectColor('yellow')\" data-color=\"yellow\" class=\"w-8 h-8 rounded-full color-option bg-yellow-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-yellow-400\" aria-label=\"Yellow\"></button> <button type=\"button\" @click=\"selectColor('green')\" data-color=\"green\" class=\"w-8 h-8 rounded-full color-option bg-green-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-green-400\" aria-label=\"Green\"></button> <button type=\"button\" @click=\"selectColor('purple')\" data-color=\"purple\" class=\"w-8 h-8 rounded-full color-option bg-purple-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-purple-400\" aria-label=\"Purple\"></button></div><input type=\"hidden\" name=\"color\" id=\"collection-color\" value=\"blue\"></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">Create Collection</button></div></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ templ CollectionRules(user User, collection CollectionData) {
|
||||
@Header(user, "/collections")
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-6">
|
||||
<button @click="backToCollection()" class="btn-secondary px-4 py-2 rounded-lg mb-4">
|
||||
← Back to Collection
|
||||
<button @click="backToCollection()" class="btn btn-secondary mb-4">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
Back to Collection
|
||||
</button>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div>
|
||||
<span class="grid place-items-center h-12 w-12 rounded-2xl shrink-0 text-2xl" style="background-color: var(--accent-muted); color: { collection.Color }">{ collection.Icon }</span>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1>
|
||||
<p style="color: var(--text-secondary)">Auto-Assign Rules</p>
|
||||
@@ -28,8 +29,10 @@ templ CollectionRules(user User, collection CollectionData) {
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Existing Rules</h2>
|
||||
<div id="rules-container" class="space-y-4">
|
||||
<div id="no-rules" class="card p-6 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="text-4xl mb-2">📋</div>
|
||||
<div id="no-rules" class="card p-8 text-center">
|
||||
<span class="inline-grid place-items-center h-12 w-12 rounded-xl mb-3" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("filter", "h-6 w-6")
|
||||
</span>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">No Rules Yet</h3>
|
||||
<p style="color: var(--text-secondary)">Create auto-assign rules to automatically add books to this collection</p>
|
||||
</div>
|
||||
@@ -37,16 +40,15 @@ templ CollectionRules(user User, collection CollectionData) {
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Create New Rule</h2>
|
||||
<form id="rule-form" @submit="handleCreateRule($event)">
|
||||
<form id="rule-form" @submit="handleCreateRule($event)" class="card p-6 space-y-6">
|
||||
<input type="hidden" id="collection-id" value="{ collection.ID }"/>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Field</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Field</label>
|
||||
<select
|
||||
id="rule-field"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="">Select field...</option>
|
||||
<option value="genre">Genre</option>
|
||||
@@ -59,12 +61,11 @@ templ CollectionRules(user User, collection CollectionData) {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Operator</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Operator</label>
|
||||
<select
|
||||
id="rule-operator"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="">Select operator...</option>
|
||||
<option value="equals">equals</option>
|
||||
@@ -78,18 +79,17 @@ templ CollectionRules(user User, collection CollectionData) {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Value</label>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Value</label>
|
||||
<input
|
||||
type="text"
|
||||
id="rule-value"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
placeholder="e.g., Science Fiction"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<div>
|
||||
<label class="flex items-center space-x-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -97,16 +97,16 @@ templ CollectionRules(user User, collection CollectionData) {
|
||||
checked
|
||||
class="w-5 h-5 rounded"
|
||||
/>
|
||||
<span style="color: var(--text-primary)">Enable Rule</span>
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">Enable Rule</span>
|
||||
</label>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Uncheck to disable without deleting</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Priority</label>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Priority</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" name="priority" value="1" class="w-4 h-4"/>
|
||||
<span style="color: var(--text-primary)">High</span>
|
||||
<span class="text-sm" style="color: var(--text-primary)">High</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
@@ -116,7 +116,7 @@ templ CollectionRules(user User, collection CollectionData) {
|
||||
checked
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span style="color: var(--text-primary)">Medium</span>
|
||||
<span class="text-sm" style="color: var(--text-primary)">Medium</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
@@ -125,36 +125,38 @@ templ CollectionRules(user User, collection CollectionData) {
|
||||
value="3"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span style="color: var(--text-primary)">Low</span>
|
||||
<span class="text-sm" style="color: var(--text-primary)">Low</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="testRule()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
🧪 Test Rule
|
||||
<button type="button" @click="testRule()" class="btn btn-secondary">
|
||||
@Icon("search", "h-4 w-4")
|
||||
Test Rule
|
||||
</button>
|
||||
<button type="button" @click="clearForm()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
<button type="button" @click="clearForm()" class="btn btn-ghost">
|
||||
Clear
|
||||
</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||||
➕ Add Rule
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@Icon("plus", "h-4 w-4")
|
||||
Add Rule
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="test-results" class="hidden card p-6 rounded-lg border mb-6" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||
<div id="test-results" class="hidden card p-6 mt-4 mb-6">
|
||||
<h3 class="font-semibold mb-3" style="color: var(--text-primary)">Rule Test Results</h3>
|
||||
<p class="text-sm mb-2" style="color: var(--text-secondary)">Books that would be added by this rule:</p>
|
||||
<div id="test-results-list" class="max-h-64 overflow-y-auto space-y-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||
<div class="card p-6">
|
||||
<h2 class="text-xl font-bold mb-4" style="color: var(--text-primary)">Rule Examples</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">1</div>
|
||||
<span class="grid place-items-center flex-shrink-0 w-8 h-8 rounded-full text-sm font-bold" style="background-color: var(--accent-muted); color: var(--accent);">1</span>
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--text-primary)">Add all Science Fiction books</p>
|
||||
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;">
|
||||
<code class="block mt-1 text-sm rounded-lg" style="color: var(--text-secondary); background-color: var(--bg-primary); padding: 8px 12px;">
|
||||
Field: genre
|
||||
<br/>
|
||||
Operator: equals
|
||||
@@ -164,10 +166,10 @@ templ CollectionRules(user User, collection CollectionData) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">2</div>
|
||||
<span class="grid place-items-center flex-shrink-0 w-8 h-8 rounded-full text-sm font-bold" style="background-color: var(--accent-muted); color: var(--accent);">2</span>
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--text-primary)">Add books from a specific series</p>
|
||||
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;">
|
||||
<code class="block mt-1 text-sm rounded-lg" style="color: var(--text-secondary); background-color: var(--bg-primary); padding: 8px 12px;">
|
||||
Field: series
|
||||
<br/>
|
||||
Operator: starts with
|
||||
@@ -177,10 +179,10 @@ templ CollectionRules(user User, collection CollectionData) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">3</div>
|
||||
<span class="grid place-items-center flex-shrink-0 w-8 h-8 rounded-full text-sm font-bold" style="background-color: var(--accent-muted); color: var(--accent);">3</span>
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--text-primary)">Add books published in a year range</p>
|
||||
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;">
|
||||
<code class="block mt-1 text-sm rounded-lg" style="color: var(--text-secondary); background-color: var(--bg-primary); padding: 8px 12px;">
|
||||
Field: copyright_year
|
||||
<br/>
|
||||
Operator: greater than
|
||||
|
||||
File diff suppressed because one or more lines are too long
+196
-134
@@ -14,82 +14,89 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
|
||||
</head>
|
||||
<body x-data="collections" x-init="initCollectionsPage()" class="theme-{ user.Theme }">
|
||||
@Header(user, "/collections")
|
||||
<!-- Modal Container -->
|
||||
<div id="modal-container"></div>
|
||||
<!-- Actual container page -->
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">My Collections</h1>
|
||||
<p style="color: var(--text-secondary)">Organize your books into custom collections</p>
|
||||
<div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8 flex flex-wrap justify-between items-center gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("folder", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">My Collections</h1>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Organize your books into custom collections</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
hx-get="/collections/restore-modal"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML"
|
||||
class="btn-secondary px-4 py-2 rounded-lg"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
🔄 Restore System
|
||||
@Icon("refresh", "h-4 w-4")
|
||||
<span>Restore System</span>
|
||||
</button>
|
||||
<button
|
||||
hx-get="/collections/create-modal"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML"
|
||||
class="btn-primary px-4 py-2 rounded-lg"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
➕ New Collection
|
||||
@Icon("plus", "h-4 w-4")
|
||||
<span>New Collection</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="collections-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
if len(collections) == 0 {
|
||||
<div class="text-center py-16 col-span-full" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">📚</div>
|
||||
<div class="card text-center py-16 px-6 col-span-full">
|
||||
<div class="grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("folder", "h-7 w-7")
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Collections Yet</h3>
|
||||
<p class="mb-4">Create collections to organize your books</p>
|
||||
<p class="mb-5" style="color: var(--text-secondary)">Create collections to organize your books</p>
|
||||
<button
|
||||
hx-get="/collections/create-modal"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML"
|
||||
class="btn-primary px-4 py-2 rounded-lg"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Create Your First Collection
|
||||
@Icon("plus", "h-4 w-4")
|
||||
<span>Create Your First Collection</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
for _, col := range collections {
|
||||
<div @click="navigateToCollection($el)" data-href={ "/collections/" + col.ID } class="block">
|
||||
<div
|
||||
class="card p-6 rounded-lg border-l-4 cursor-pointer hover:shadow-lg transition-shadow"
|
||||
style="background-color: var(--bg-secondary);"
|
||||
class="card p-6 rounded-2xl border-l-4 cursor-pointer"
|
||||
data-color={ col.Color }
|
||||
>
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="text-3xl">{ col.Icon }</div>
|
||||
<div class="flex space-x-2">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
hx-get={ "/collections/" + col.ID + "/edit-modal" }
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML"
|
||||
class="p-2 hover:opacity-80 rounded"
|
||||
style="color: var(--text-secondary); background-color: var(--bg-primary);"
|
||||
class="icon-btn"
|
||||
aria-label="Edit collection"
|
||||
>
|
||||
✏️
|
||||
@Icon("edit", "h-4 w-4")
|
||||
</button>
|
||||
<button
|
||||
hx-delete={ "/api/collections/" + col.ID + "" }
|
||||
hx-redirect="/collections"
|
||||
hx-confirm="Are you sure you want to delete this collection?"
|
||||
class="p-2 hover:opacity-80 rounded"
|
||||
style="color: var(--text-secondary); background-color: var(--bg-primary);"
|
||||
class="icon-btn"
|
||||
aria-label="Delete collection"
|
||||
>
|
||||
🗑️
|
||||
@Icon("trash", "h-4 w-4")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ col.Name }</h3>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ col.Description }</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">{ col.Description }</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -113,123 +120,128 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
<body x-data="collections" x-init="initCollectionsPage()" class="theme-{ user.Theme }">
|
||||
@Header(user, "/collections")
|
||||
@LibrarySwitcher(libData, libraryID)
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-6">
|
||||
<a href="/collections" class="btn-secondary px-4 py-2 rounded-lg mb-4 inline-block">
|
||||
← Back to Collections
|
||||
<a href="/collections" class="btn btn-secondary mb-4">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
<span>Back to Collections</span>
|
||||
</a>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div>
|
||||
<div class="grid place-items-center h-14 w-14 rounded-2xl text-3xl" style="background-color: var(--accent-muted); color: { collection.Color }">{ collection.Icon }</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1>
|
||||
<p style="color: var(--text-secondary)">{ collection.Description }</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">{ collection.Name }</h1>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">{ collection.Description }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6 flex justify-between items-center">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Books in this Collection</h2>
|
||||
<span id="selected-count" class="hidden px-3 py-1 text-sm rounded" style="background-color: var(--accent); color: var(--bg-primary);">
|
||||
0 selected
|
||||
</span>
|
||||
<div class="mb-6 flex flex-wrap justify-between items-center gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-lg font-bold tracking-tight" style="color: var(--text-primary)">Books in this Collection</h2>
|
||||
<span x-show="selectedBooks.length > 0" x-text="selectedBooks.length + ' selected'" class="badge" style="display: none; background-color: var(--accent); color: var(--bg-primary);"></span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="flex-1 max-w-md">
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<div class="flex-1 min-w-[200px] max-w-md">
|
||||
<input
|
||||
type="text"
|
||||
id="collection-search"
|
||||
placeholder="Search within collection..."
|
||||
onkeyup="filterCollectionBooks()"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
@input="filterCollectionBooks()"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
id="bulk-remove-btn"
|
||||
disabled
|
||||
class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
🗑️ Remove Selected
|
||||
</button>
|
||||
<button
|
||||
@click="$store.bookPicker.open()"
|
||||
class="btn-primary px-4 py-2 rounded-lg"
|
||||
>
|
||||
➕ Add Books
|
||||
</button>
|
||||
if !collection.IsSystem {
|
||||
<button
|
||||
@click="requestBulkRemove()"
|
||||
:disabled="selectedBooks.length === 0"
|
||||
:class="selectedBooks.length === 0 ? 'opacity-50 cursor-not-allowed' : ''"
|
||||
class="btn btn-danger"
|
||||
>
|
||||
@Icon("trash", "h-4 w-4")
|
||||
<span>Remove Selected</span>
|
||||
</button>
|
||||
<button
|
||||
@click="$store.bookPicker.open()"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
@Icon("plus", "h-4 w-4")
|
||||
<span>Add Books</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div id="books-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
if len(books) == 0 {
|
||||
<div id="empty-state" class="col-span-full text-center py-16" style="color: var(--text-secondary)">No books in this collection yet.</div>
|
||||
<div id="empty-state" class="col-span-full card text-center py-16" style="color: var(--text-secondary)">No books in this collection yet.</div>
|
||||
}
|
||||
for _, book := range books {
|
||||
<a href={ "/media/" + book.MediaItemID }>
|
||||
<div
|
||||
class="card p-4 rounded-lg border hover:shadow-lg transition-shadow"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="card p-4 rounded-2xl collection-book-card" data-title={ book.Title } data-author={ book.Author } data-media-id={ book.MediaItemID }>
|
||||
<div class="flex gap-4">
|
||||
if !collection.IsSystem {
|
||||
<div class="flex-shrink-0 pt-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
onchange="toggleBookForRemoval('{ book.MediaItemID }')"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
<label class="flex items-center cursor-pointer p-2 -m-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="w-5 h-5"
|
||||
:checked="selectedBooks.includes('{ book.MediaItemID }')"
|
||||
@change="toggleSelection('{ book.MediaItemID }')"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3
|
||||
class="font-semibold text-lg mb-1 line-clamp-2"
|
||||
style="color: var(--text-primary)"
|
||||
>
|
||||
}
|
||||
<div class="flex-1 min-w-0">
|
||||
<a href={ "/media/" + book.MediaItemID }>
|
||||
<h3 class="font-semibold text-lg mb-1 line-clamp-2 hover:underline" style="color: var(--text-primary)">
|
||||
{ book.Title }
|
||||
</h3>
|
||||
if book.Author != "" {
|
||||
<p
|
||||
class="text-sm line-clamp-1"
|
||||
style="color: var(--text-secondary)"
|
||||
>
|
||||
by { book.Author }
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div class="flex-shrink-0 w-16 sm:w-20">
|
||||
</a>
|
||||
if book.Author != "" {
|
||||
<p class="text-sm line-clamp-1" style="color: var(--text-secondary)">
|
||||
by { book.Author }
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div class="flex-shrink-0 w-16 sm:w-20">
|
||||
<a href={ "/media/" + book.MediaItemID }>
|
||||
if book.CoverImagePath != "" {
|
||||
<img
|
||||
src={ book.CoverImagePath }
|
||||
alt="Cover"
|
||||
class="w-full aspect-[3/4] object-cover rounded shadow-md"
|
||||
class="w-full aspect-[3/4] object-cover rounded-lg shadow-md"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
/>
|
||||
} else {
|
||||
<img
|
||||
src="/static/placeholder-book.svg"
|
||||
alt="Cover"
|
||||
class="w-full aspect-[3/4] object-cover rounded shadow-md"
|
||||
class="w-full aspect-[3/4] object-cover rounded-lg shadow-md"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
|
||||
<button
|
||||
@click="removeBook('{ book.MediaItemID }')"
|
||||
class="px-3 py-1 text-sm border rounded hover:opacity-80"
|
||||
style="border-color: var(--border); color: var(--text-secondary);"
|
||||
>
|
||||
🗑️ Remove from Collection
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
if !collection.IsSystem {
|
||||
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
|
||||
<button
|
||||
@click="requestRemoveBook('{ book.MediaItemID }')"
|
||||
class="btn btn-secondary w-full"
|
||||
>
|
||||
@Icon("trash", "h-4 w-4")
|
||||
<span>Remove from Collection</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Book Picker Modal -->
|
||||
<div
|
||||
x-data="bookPicker"
|
||||
if !collection.IsSystem {
|
||||
<div
|
||||
x-data="bookPicker"
|
||||
x-show="$store.bookPicker.isOpen"
|
||||
@keyup.escape.window="$store.bookPicker.close()"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="display: none;"
|
||||
@click.self="$store.bookPicker.close()"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
style="display: none; background-color: var(--surface-overlay);"
|
||||
>
|
||||
<div
|
||||
@click.stop
|
||||
@@ -240,8 +252,8 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="card rounded-lg w-full max-w-6xl mx-4 my-8"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border); display: none;"
|
||||
class="card rounded-2xl w-full max-w-6xl mx-4 my-8"
|
||||
style="display: none; box-shadow: var(--shadow-pop);"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center p-6 border-b"
|
||||
@@ -252,23 +264,24 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
</h2>
|
||||
<button
|
||||
@click="$store.bookPicker.close()"
|
||||
class="p-2 hover:opacity-80 rounded"
|
||||
style="color: var(--text-primary)"
|
||||
>✕</button>
|
||||
class="icon-btn"
|
||||
aria-label="Close"
|
||||
>
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="p-4 border-b"
|
||||
style="border-color: var(--border);"
|
||||
>
|
||||
<div class="flex flex-wrap gap-4 items-center">
|
||||
<div id="book-picker-filters" class="flex flex-wrap gap-3 items-center">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
name="q"
|
||||
placeholder="Search books..."
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
hx-get="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }"
|
||||
class="input"
|
||||
hx-get="/api/media-items/search?show_checkbox=true&collection_id={ collection.ID }"
|
||||
hx-target="#book-picker-grid"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-include="#book-picker-filters"
|
||||
@@ -279,9 +292,8 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
type="text"
|
||||
name="author_filter"
|
||||
placeholder="Author"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
hx-get="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }"
|
||||
class="input"
|
||||
hx-get="/api/media-items/search?show_checkbox=true&collection_id={ collection.ID }"
|
||||
hx-target="#book-picker-grid"
|
||||
hx-trigger="change"
|
||||
hx-include="#book-picker-filters"
|
||||
@@ -292,32 +304,30 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
type="text"
|
||||
name="genre_filter"
|
||||
placeholder="Genre"
|
||||
class="w-full px-3 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
hx-get="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }"
|
||||
class="input"
|
||||
hx-get="/api/media-items/search?show_checkbox=true&collection_id={ collection.ID }"
|
||||
hx-target="#book-picker-grid"
|
||||
hx-trigger="change"
|
||||
hx-include="#book-picker-filters"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
@click="$store.bookPicker.clearFilters()"
|
||||
class="px-4 py-2 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
>
|
||||
✕ Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form id="filter-form" class="hidden">
|
||||
<button
|
||||
@click="$store.bookPicker.clearFilters()"
|
||||
class="btn btn-ghost"
|
||||
>
|
||||
@Icon("close", "h-4 w-4")
|
||||
<span>Clear</span>
|
||||
</button>
|
||||
<input type="hidden" name="limit" value="50"/>
|
||||
<input type="hidden" name="offset" value="0"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="book-picker-grid"
|
||||
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 p-4 max-h-96 overflow-y-auto"
|
||||
hx-get="/api/media-items/search?show_checkbox=true&collection_id={ collection.ID }"
|
||||
hx-trigger="loadBooks"
|
||||
hx-include="#book-picker-filters"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
@@ -330,15 +340,13 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="$store.bookPicker.close()"
|
||||
class="px-4 py-2 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="$store.bookPicker.submit()"
|
||||
class="px-4 py-2 rounded-lg font-medium"
|
||||
style="background-color: var(--accent); color: var(--bg-primary);"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Add Selected Books
|
||||
</button>
|
||||
@@ -346,12 +354,66 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
x-show="showConfirm"
|
||||
@click.self="closeConfirm()"
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center p-4"
|
||||
style="display: none; background-color: var(--surface-overlay);"
|
||||
>
|
||||
<div
|
||||
@click.stop
|
||||
class="card p-6 w-full max-w-md mx-4"
|
||||
style="box-shadow: var(--shadow-pop);"
|
||||
>
|
||||
<h3 class="text-lg font-bold mb-2" style="color: var(--text-primary)">Remove from Collection</h3>
|
||||
<p x-text="confirmMessage" class="text-sm mb-6" style="color: var(--text-secondary)"></p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button @click="closeConfirm()" class="btn btn-secondary">Cancel</button>
|
||||
<button @click="executeConfirmed()" class="btn btn-danger">
|
||||
@Icon("trash", "h-4 w-4")
|
||||
<span x-text="confirmLabel"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<div
|
||||
id="collection-data"
|
||||
data-id={ collection.ID }
|
||||
data-library-id={ libraryID }
|
||||
data-is-system={ collection.IsSystem }
|
||||
style="display: none;"
|
||||
></div>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ BookPickerGrid(books []handlers.BookInfo) {
|
||||
for _, book := range books {
|
||||
<div
|
||||
class="relative cursor-pointer rounded-lg overflow-hidden"
|
||||
@click={ "$store.bookPicker.toggleBook('" + book.MediaItemID + "')" }
|
||||
>
|
||||
<div class="relative">
|
||||
if book.CoverImagePath != "" {
|
||||
<img src={ book.CoverImagePath } alt={ book.Title } class="w-full aspect-[2/3] object-cover" loading="lazy" onerror="this.src='/static/placeholder-book.svg'"/>
|
||||
} else {
|
||||
<img src="/static/placeholder-book.svg" alt={ book.Title } class="w-full aspect-[2/3] object-cover" loading="lazy"/>
|
||||
}
|
||||
<div
|
||||
x-show={ "$store.bookPicker.isSelected('" + book.MediaItemID + "')" }
|
||||
class="absolute inset-0 border-4 rounded-lg pointer-events-none"
|
||||
style="border-color: var(--accent); background-color: color-mix(in srgb, var(--accent) 20%, transparent);"
|
||||
></div>
|
||||
<div
|
||||
x-show={ "$store.bookPicker.isSelected('" + book.MediaItemID + "')" }
|
||||
class="absolute top-1 right-1 w-6 h-6 rounded-full flex items-center justify-center text-sm font-bold pointer-events-none"
|
||||
style="background-color: var(--accent); color: var(--bg-primary);"
|
||||
>
|
||||
✓
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs mt-1 line-clamp-2" style="color: var(--text-primary)">{ book.Title }</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
+423
-69
@@ -39,114 +39,170 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Modal Container --><div id=\"modal-container\"></div><!-- Actual container page --><div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8 flex justify-between items-center\"><div><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">My Collections</h1><p style=\"color: var(--text-secondary)\">Organize your books into custom collections</p></div><div class=\"flex gap-3\"><button hx-get=\"/collections/restore-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn-secondary px-4 py-2 rounded-lg\">🔄 Restore System</button> <button hx-get=\"/collections/create-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn-primary px-4 py-2 rounded-lg\">➕ New Collection</button></div></div><div id=\"collections-list\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div id=\"modal-container\"></div><div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8 flex flex-wrap justify-between items-center gap-4\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("folder", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><div><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">My Collections</h1><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Organize your books into custom collections</p></div></div><div class=\"flex gap-2\"><button hx-get=\"/collections/restore-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn btn-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("refresh", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span>Restore System</span></button> <button hx-get=\"/collections/create-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span>New Collection</span></button></div></div><div id=\"collections-list\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(collections) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"text-center py-16 col-span-full\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📚</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Collections Yet</h3><p class=\"mb-4\">Create collections to organize your books</p><button hx-get=\"/collections/create-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn-primary px-4 py-2 rounded-lg\">Create Your First Collection</button></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"card text-center py-16 px-6 col-span-full\"><div class=\"grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("folder", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Collections Yet</h3><p class=\"mb-5\" style=\"color: var(--text-secondary)\">Create collections to organize your books</p><button hx-get=\"/collections/create-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span>Create Your First Collection</span></button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, col := range collections {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div @click=\"navigateToCollection($el)\" data-href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div @click=\"navigateToCollection($el)\" data-href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("/collections/" + col.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 62, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 70, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"block\"><div class=\"card p-6 rounded-lg border-l-4 cursor-pointer hover:shadow-lg transition-shadow\" style=\"background-color: var(--bg-secondary);\" data-color=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"block\"><div class=\"card p-6 rounded-2xl border-l-4 cursor-pointer\" data-color=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(col.Color)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 66, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 73, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"><div class=\"flex justify-between items-start mb-4\"><div class=\"text-3xl\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"><div class=\"flex justify-between items-start mb-4\"><div class=\"text-3xl\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 69, Col: 41}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 76, 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, 7, "</div><div class=\"flex space-x-2\"><button hx-get=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><div class=\"flex gap-1\"><button hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("/collections/" + col.ID + "/edit-modal")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 72, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 79, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-secondary); background-color: var(--bg-primary);\">✏️</button> <button hx-delete=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"icon-btn\" aria-label=\"Edit collection\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("edit", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</button> <button hx-delete=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/collections/" + col.ID + "")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 81, Col: 56}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 88, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-redirect=\"/collections\" hx-confirm=\"Are you sure you want to delete this collection?\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-secondary); background-color: var(--bg-primary);\">🗑️</button></div></div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" hx-redirect=\"/collections\" hx-confirm=\"Are you sure you want to delete this collection?\" class=\"icon-btn\" aria-label=\"Delete collection\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</button></div></div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 91, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 98, Col: 92}
|
||||
}
|
||||
_, 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, 10, "</h3><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 92, Col: 86}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 99, Col: 81}
|
||||
}
|
||||
_, 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, 11, "</p></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -154,7 +210,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -183,20 +239,20 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
templ_7745c5c3_Var9 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 109, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 116, Col: 27}
|
||||
}
|
||||
_, 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, 15, " - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"collections\" x-init=\"initCollectionsPage()\" class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"collections\" x-init=\"initCollectionsPage()\" class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -208,165 +264,326 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-6\"><a href=\"/collections\" class=\"btn-secondary px-4 py-2 rounded-lg mb-4 inline-block\">← Back to Collections</a><div class=\"flex items-center gap-4\"><div class=\"text-4xl\" style=\"color: { collection.Color }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-6\"><a href=\"/collections\" class=\"btn btn-secondary mb-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<span>Back to Collections</span></a><div class=\"flex items-center gap-4\"><div class=\"grid place-items-center h-14 w-14 rounded-2xl text-3xl\" style=\"background-color: var(--accent-muted); color: { collection.Color }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 122, Col: 81}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 130, Col: 166}
|
||||
}
|
||||
_, 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, 17, "</div><div><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div><div><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 124, Col: 90}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 132, Col: 105}
|
||||
}
|
||||
_, 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, 18, "</h1><p style=\"color: var(--text-secondary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</h1><p class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 125, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 133, Col: 87}
|
||||
}
|
||||
_, 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, 19, "</p></div></div></div><div class=\"mb-6 flex justify-between items-center\"><div class=\"flex items-center gap-4\"><h2 class=\"text-xl font-semibold\" style=\"color: var(--text-primary)\">Books in this Collection</h2><span id=\"selected-count\" class=\"hidden px-3 py-1 text-sm rounded\" style=\"background-color: var(--accent); color: var(--bg-primary);\">0 selected</span></div><div class=\"flex gap-3\"><div class=\"flex-1 max-w-md\"><input type=\"text\" id=\"collection-search\" placeholder=\"Search within collection...\" onkeyup=\"filterCollectionBooks()\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><button id=\"bulk-remove-btn\" disabled class=\"btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed\">🗑️ Remove Selected</button> <button @click=\"$store.bookPicker.open()\" class=\"btn-primary px-4 py-2 rounded-lg\">➕ Add Books</button></div></div><div id=\"books-container\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</p></div></div></div><div class=\"mb-6 flex flex-wrap justify-between items-center gap-4\"><div class=\"flex items-center gap-3\"><h2 class=\"text-lg font-bold tracking-tight\" style=\"color: var(--text-primary)\">Books in this Collection</h2><span x-show=\"selectedBooks.length > 0\" x-text=\"selectedBooks.length + ' selected'\" class=\"badge\" style=\"display: none; background-color: var(--accent); color: var(--bg-primary);\"></span></div><div class=\"flex flex-wrap gap-2 items-center\"><div class=\"flex-1 min-w-[200px] max-w-md\"><input type=\"text\" id=\"collection-search\" placeholder=\"Search within collection...\" @input=\"filterCollectionBooks()\" class=\"input\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !collection.IsSystem {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<button @click=\"requestBulkRemove()\" :disabled=\"selectedBooks.length === 0\" :class=\"selectedBooks.length === 0 ? 'opacity-50 cursor-not-allowed' : ''\" class=\"btn btn-danger\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<span>Remove Selected</span></button> <button @click=\"$store.bookPicker.open()\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<span>Add Books</span></button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div></div><div id=\"books-container\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(books) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div id=\"empty-state\" class=\"col-span-full text-center py-16\" style=\"color: var(--text-secondary)\">No books in this collection yet.</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div id=\"empty-state\" class=\"col-span-full card text-center py-16\" style=\"color: var(--text-secondary)\">No books in this collection yet.</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, book := range books {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div class=\"card p-4 rounded-2xl collection-book-card\" data-title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 templ.SafeURL
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + book.MediaItemID)
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 167, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 177, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\"><div class=\"card p-4 rounded-lg border hover:shadow-lg transition-shadow\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex gap-4\"><div class=\"flex-shrink-0 pt-1\"><input type=\"checkbox\" onchange=\"toggleBookForRemoval('{ book.MediaItemID }')\" class=\"w-5 h-5\"></div><div class=\"flex-1 min-w-0\"><h3 class=\"font-semibold text-lg mb-1 line-clamp-2\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" data-author=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 185, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 177, Col: 112}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</h3>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" data-media-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 177, Col: 147}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\"><div class=\"flex gap-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !collection.IsSystem {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"flex-shrink-0 pt-1\"><label class=\"flex items-center cursor-pointer p-2 -m-2\"><input type=\"checkbox\" class=\"w-5 h-5\" :checked=\"selectedBooks.includes('{ book.MediaItemID }')\" @change=\"toggleSelection('{ book.MediaItemID }')\"></label></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"flex-1 min-w-0\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 templ.SafeURL
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + book.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 192, Col: 47}
|
||||
}
|
||||
_, 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, 39, "\"><h3 class=\"font-semibold text-lg mb-1 line-clamp-2 hover:underline\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 194, Col: 23}
|
||||
}
|
||||
_, 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, 40, "</h3></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if book.Author != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<p class=\"text-sm line-clamp-1\" style=\"color: var(--text-secondary)\">by ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<p class=\"text-sm line-clamp-1\" style=\"color: var(--text-secondary)\">by ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 192, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 199, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
_, 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, "</p>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div><div class=\"flex-shrink-0 w-16 sm:w-20\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div><div class=\"flex-shrink-0 w-16 sm:w-20\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 templ.SafeURL
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + book.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 204, Col: 47}
|
||||
}
|
||||
_, 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, 44, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if book.CoverImagePath != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<img src=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath)
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 199, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 207, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded shadow-md\" onerror=\"this.src='/static/placeholder-book.svg'\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded-lg shadow-md\" onerror=\"this.src='/static/placeholder-book.svg'\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<img src=\"/static/placeholder-book.svg\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded shadow-md\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<img src=\"/static/placeholder-book.svg\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded-lg shadow-md\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></div><div class=\"mt-3 pt-3 border-t\" style=\"border-color: var(--border);\"><button @click=\"removeBook('{ book.MediaItemID }')\" class=\"px-3 py-1 text-sm border rounded hover:opacity-80\" style=\"border-color: var(--border); color: var(--text-secondary);\">🗑️ Remove from Collection</button></div></div></a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</a></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !collection.IsSystem {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<div class=\"mt-3 pt-3 border-t\" style=\"border-color: var(--border);\"><button @click=\"requestRemoveBook('{ book.MediaItemID }')\" class=\"btn btn-secondary w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<span>Remove from Collection</span></button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div></div><!-- Book Picker Modal --><div x-data=\"bookPicker\" @keyup.escape.window=\"$store.bookPicker.close()\" class=\"fixed inset-0 z-50 flex items-center justify-center\" style=\"display: none;\"><div @click.stop x-show=\"$store.bookPicker.isOpen\" x-transition:enter=\"transition ease-out duration-200\" x-transition:enter-start=\"opacity-0 scale-95\" x-transition:enter-end=\"opacity-100 scale-100\" x-transition:leave=\"transition ease-in duration-150\" x-transition:leave-start=\"opacity-100 scale-100\" x-transition:leave-end=\"opacity-0 scale-95\" class=\"card rounded-lg w-full max-w-6xl mx-4 my-8\" style=\"background-color: var(--bg-secondary); border-color: var(--border); display: none;\"><div class=\"flex justify-between items-center p-6 border-b\" style=\"border-color: var(--border);\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Add Books to Collection</h2><button @click=\"$store.bookPicker.close()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><div class=\"p-4 border-b\" style=\"border-color: var(--border);\"><div class=\"flex flex-wrap gap-4 items-center\"><div class=\"flex-1 min-w-[200px]\"><input type=\"text\" name=\"search\" placeholder=\"Search books...\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" hx-get=\"/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"keyup changed delay:300ms\" hx-include=\"#book-picker-filters\"></div><div class=\"flex-1 min-w-[150px]\"><input type=\"text\" name=\"author_filter\" placeholder=\"Author\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" hx-get=\"/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"change\" hx-include=\"#book-picker-filters\"></div><div class=\"flex-1 min-w-[150px]\"><input type=\"text\" name=\"genre_filter\" placeholder=\"Genre\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" hx-get=\"/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"change\" hx-include=\"#book-picker-filters\"></div><div><button @click=\"$store.bookPicker.clearFilters()\" class=\"px-4 py-2 rounded-lg border\" style=\"border-color: var(--border); color: var(--text-primary);\">✕ Clear</button></div></div><form id=\"filter-form\" class=\"hidden\"><input type=\"hidden\" name=\"limit\" value=\"50\"> <input type=\"hidden\" name=\"offset\" value=\"0\"></form></div><div id=\"book-picker-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 p-4 max-h-96 overflow-y-auto\"></div><div class=\"p-4 border-t flex justify-between items-center\" style=\"border-color: var(--border);\"><div class=\"text-sm\" style=\"color: var(--text-secondary);\"><span x-text=\"$store.bookPicker.selectedCount\"></span> books selected</div><div class=\"flex gap-2\"><button @click=\"$store.bookPicker.close()\" class=\"px-4 py-2 rounded-lg border\" style=\"border-color: var(--border); color: var(--text-primary);\">Cancel</button> <button @click=\"$store.bookPicker.submit()\" class=\"px-4 py-2 rounded-lg font-medium\" style=\"background-color: var(--accent); color: var(--bg-primary);\">Add Selected Books</button></div></div></div></div></body><div id=\"collection-data\" data-id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 352, Col: 26}
|
||||
if !collection.IsSystem {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<div x-data=\"bookPicker\" x-show=\"$store.bookPicker.isOpen\" @keyup.escape.window=\"$store.bookPicker.close()\" @click.self=\"$store.bookPicker.close()\" class=\"fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"display: none; background-color: var(--surface-overlay);\"><div @click.stop x-show=\"$store.bookPicker.isOpen\" x-transition:enter=\"transition ease-out duration-200\" x-transition:enter-start=\"opacity-0 scale-95\" x-transition:enter-end=\"opacity-100 scale-100\" x-transition:leave=\"transition ease-in duration-150\" x-transition:leave-start=\"opacity-100 scale-100\" x-transition:leave-end=\"opacity-0 scale-95\" class=\"card rounded-2xl w-full max-w-6xl mx-4 my-8\" style=\"display: none; box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center p-6 border-b\" style=\"border-color: var(--border);\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Add Books to Collection</h2><button @click=\"$store.bookPicker.close()\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</button></div><div class=\"p-4 border-b\" style=\"border-color: var(--border);\"><div id=\"book-picker-filters\" class=\"flex flex-wrap gap-3 items-center\"><div class=\"flex-1 min-w-[200px]\"><input type=\"text\" name=\"q\" placeholder=\"Search books...\" class=\"input\" hx-get=\"/api/media-items/search?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"keyup changed delay:300ms\" hx-include=\"#book-picker-filters\"></div><div class=\"flex-1 min-w-[150px]\"><input type=\"text\" name=\"author_filter\" placeholder=\"Author\" class=\"input\" hx-get=\"/api/media-items/search?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"change\" hx-include=\"#book-picker-filters\"></div><div class=\"flex-1 min-w-[150px]\"><input type=\"text\" name=\"genre_filter\" placeholder=\"Genre\" class=\"input\" hx-get=\"/api/media-items/search?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"change\" hx-include=\"#book-picker-filters\"></div><button @click=\"$store.bookPicker.clearFilters()\" class=\"btn btn-ghost\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<span>Clear</span></button> <input type=\"hidden\" name=\"limit\" value=\"50\"> <input type=\"hidden\" name=\"offset\" value=\"0\"></div></div><div id=\"book-picker-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 p-4 max-h-96 overflow-y-auto\" hx-get=\"/api/media-items/search?show_checkbox=true&collection_id={ collection.ID }\" hx-trigger=\"loadBooks\" hx-include=\"#book-picker-filters\"></div><div class=\"p-4 border-t flex justify-between items-center\" style=\"border-color: var(--border);\"><div class=\"text-sm\" style=\"color: var(--text-secondary);\"><span x-text=\"$store.bookPicker.selectedCount\"></span> books selected</div><div class=\"flex gap-2\"><button @click=\"$store.bookPicker.close()\" class=\"btn btn-secondary\">Cancel</button> <button @click=\"$store.bookPicker.submit()\" class=\"btn btn-primary\">Add Selected Books</button></div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<div x-show=\"showConfirm\" @click.self=\"closeConfirm()\" class=\"fixed inset-0 z-[60] flex items-center justify-center p-4\" style=\"display: none; background-color: var(--surface-overlay);\"><div @click.stop class=\"card p-6 w-full max-w-md mx-4\" style=\"box-shadow: var(--shadow-pop);\"><h3 class=\"text-lg font-bold mb-2\" style=\"color: var(--text-primary)\">Remove from Collection</h3><p x-text=\"confirmMessage\" class=\"text-sm mb-6\" style=\"color: var(--text-secondary)\"></p><div class=\"flex justify-end gap-3\"><button @click=\"closeConfirm()\" class=\"btn btn-secondary\">Cancel</button> <button @click=\"executeConfirmed()\" class=\"btn btn-danger\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" data-library-id=\"")
|
||||
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 353, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<span x-text=\"confirmLabel\"></span></button></div></div></div></body><div id=\"collection-data\" data-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" style=\"display: none;\"></div></html>")
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 383, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\" data-library-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 384, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\" data-is-system=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.IsSystem)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 385, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" style=\"display: none;\"></div></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -374,4 +591,141 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
})
|
||||
}
|
||||
|
||||
func BookPickerGrid(books []handlers.BookInfo) 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)
|
||||
for _, book := range books {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<div class=\"relative cursor-pointer rounded-lg overflow-hidden\" @click=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue("$store.bookPicker.toggleBook('" + book.MediaItemID + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 395, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\"><div class=\"relative\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if book.CoverImagePath != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 399, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\" alt=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 399, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" class=\"w-full aspect-[2/3] object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<img src=\"/static/placeholder-book.svg\" alt=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 401, Col: 61}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\" class=\"w-full aspect-[2/3] object-cover\" loading=\"lazy\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<div x-show=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue("$store.bookPicker.isSelected('" + book.MediaItemID + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 404, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" class=\"absolute inset-0 border-4 rounded-lg pointer-events-none\" style=\"border-color: var(--accent); background-color: color-mix(in srgb, var(--accent) 20%, transparent);\"></div><div x-show=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue("$store.bookPicker.isSelected('" + book.MediaItemID + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 409, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" class=\"absolute top-1 right-1 w-6 h-6 rounded-full flex items-center justify-center text-sm font-bold pointer-events-none\" style=\"background-color: var(--accent); color: var(--bg-primary);\">✓</div></div><p class=\"text-xs mt-1 line-clamp-2\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 416, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
|
||||
+82
-69
@@ -14,55 +14,66 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
|
||||
</head>
|
||||
<body x-data="conflicts" class="theme-{ user.Theme }">
|
||||
@Header(user, "/conflicts")
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex justify-between items-center gap-4 flex-wrap">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Sync Conflicts</h1>
|
||||
<p style="color: var(--text-secondary)">Resolve conflicts when reading progress differs across devices</p>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: color-mix(in srgb, var(--status-warning) 16%, transparent); color: var(--status-warning);">
|
||||
@Icon("alert", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Sync Conflicts</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Resolve conflicts when reading progress differs across devices</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<select
|
||||
id="status-filter"
|
||||
onchange="filterConflicts()"
|
||||
class="px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="unresolved">Unresolved Only</option>
|
||||
<option value="all">All Conflicts</option>
|
||||
<option value="resolved">Resolved Only</option>
|
||||
</select>
|
||||
<button @click="dismissAllResolved()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Dismiss All Resolved
|
||||
</button>
|
||||
<button @click="dismissAllResolved()" class="btn btn-secondary">Dismiss All Resolved</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stats-bar" class="mt-4 flex space-x-6 text-sm">
|
||||
<span style="color: var(--text-secondary)">Total: <strong style="color: var(--text-primary)">{ total }</strong></span>
|
||||
<span style="color: var(--text-secondary)">Unresolved: <strong style="color: #f59e0b;">{ unresolved }</strong></span>
|
||||
<span style="color: var(--text-secondary)">Resolved: <strong style="color: #10b981;">{ total - unresolved }</strong></span>
|
||||
<div id="stats-bar" class="mt-4 grid grid-cols-3 gap-4 max-w-lg">
|
||||
<div class="stat-card text-center py-3">
|
||||
<div class="text-2xl font-bold" style="color: var(--text-primary);">{ total }</div>
|
||||
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Total</div>
|
||||
</div>
|
||||
<div class="stat-card text-center py-3">
|
||||
<div class="text-2xl font-bold" style="color: var(--status-warning);">{ unresolved }</div>
|
||||
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Unresolved</div>
|
||||
</div>
|
||||
<div class="stat-card text-center py-3">
|
||||
<div class="text-2xl font-bold" style="color: var(--status-success);">{ total - unresolved }</div>
|
||||
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Resolved</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bulk-actions" class="mt-4 hidden">
|
||||
<div class="flex items-center justify-between p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="card p-4 flex items-center justify-between gap-4 flex-wrap">
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm font-semibold" style="color: var(--text-primary);">
|
||||
<span id="selected-count">0</span> conflicts selected
|
||||
</span>
|
||||
<select
|
||||
id="bulk-strategy"
|
||||
class="px-3 py-2 border rounded-lg text-sm"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input w-auto"
|
||||
>
|
||||
<option value="most_recent">Most Recent</option>
|
||||
<option value="highest_progress">Highest Progress</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button @click="bulkResolve()" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
||||
✅ Resolve Selected
|
||||
<button @click="bulkResolve()" class="btn btn-primary text-sm">
|
||||
@Icon("check", "h-4 w-4")
|
||||
Resolve Selected
|
||||
</button>
|
||||
<button @click="bulkDismiss()" class="btn-danger px-4 py-2 rounded-lg text-sm">
|
||||
❌ Dismiss Selected
|
||||
<button @click="bulkDismiss()" class="btn btn-danger text-sm">
|
||||
Dismiss Selected
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,12 +83,14 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
|
||||
<div class="loading-spinner mx-auto mb-4"></div>
|
||||
<p>Loading conflicts...</p>
|
||||
</div>
|
||||
<div id="empty-state" class="hidden text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">✅</div>
|
||||
<div id="empty-state" class="hidden card text-center py-16">
|
||||
<span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: color-mix(in srgb, var(--status-success) 16%, transparent); color: var(--status-success);">
|
||||
@Icon("check-circle", "h-7 w-7")
|
||||
</span>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Conflicts</h3>
|
||||
<p>Your devices are in sync! No conflicts to resolve.</p>
|
||||
<p style="color: var(--text-secondary)">Your devices are in sync! No conflicts to resolve.</p>
|
||||
</div>
|
||||
<div id="conflicts-list" class="space-y-6">
|
||||
<div id="conflicts-list" class="space-y-4">
|
||||
<div id="select-all-container" class="mb-4 hidden">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="checkbox" id="select-all-conflicts" class="w-5 h-5 rounded" onchange="toggleAllConflicts()"/>
|
||||
@@ -85,15 +98,17 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
|
||||
</label>
|
||||
</div>
|
||||
if len(conflicts) == 0 {
|
||||
<div class="text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">✅</div>
|
||||
<div class="card text-center py-16">
|
||||
<span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: color-mix(in srgb, var(--status-success) 16%, transparent); color: var(--status-success);">
|
||||
@Icon("check-circle", "h-7 w-7")
|
||||
</span>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Conflicts</h3>
|
||||
<p>Your devices are in sync! No conflicts to resolve.</p>
|
||||
<p style="color: var(--text-secondary)">Your devices are in sync! No conflicts to resolve.</p>
|
||||
</div>
|
||||
}
|
||||
for _, conflict := range conflicts {
|
||||
<div class="conflict-item card p-6 rounded-lg border" data-conflict-id="{ conflict.ID }" style="background-color: var(--bg-secondary); border-color: var(--border); border-left-width: 4px; border-left-style: solid; border-left-color: rgb(245, 158, 11);">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="conflict-item card p-6" data-conflict-id="{ conflict.ID }" style="border-left-width: 4px; border-left-color: var(--status-warning);">
|
||||
<div class="flex justify-between items-start mb-4 gap-4">
|
||||
<div class="flex items-start space-x-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -101,14 +116,17 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
|
||||
data-conflict-id="{ conflict.ID }"
|
||||
onchange="updateBulkActions()"
|
||||
/>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">{ conflict.MediaItemTitle }</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Type: { conflict.ConflictType }</p>
|
||||
<div class="flex items-start gap-3">
|
||||
<span style="color: var(--status-warning);">
|
||||
@Icon("alert", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">{ conflict.MediaItemTitle }</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Type: { conflict.ConflictType }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="showResolveModal('{ conflict.ID }')" class="btn-primary px-4 py-2 rounded-lg">
|
||||
Resolve
|
||||
</button>
|
||||
<button @click="showResolveModal('{ conflict.ID }')" class="btn btn-primary">Resolve</button>
|
||||
</div>
|
||||
<div class="text-sm" style="color: var(--text-secondary)">
|
||||
Created: { FormatInTimezone(conflict.CreatedAt, user.Timezone) }
|
||||
@@ -119,94 +137,90 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div id="conflict-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-4xl mx-4 my-8" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div id="conflict-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-4xl my-8" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Resolve Conflict</h2>
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Resolve Conflict</h2>
|
||||
<p class="text-sm" id="conflict-book-title" style="color: var(--text-secondary);">Book Title</p>
|
||||
</div>
|
||||
<button @click="hideResolveModal()" class="p-2 hover:opacity-80 rounded-lg" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
X
|
||||
<button @click="hideResolveModal()" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<form id="conflict-resolution-form" @submit="handleResolveSubmit($event)">
|
||||
<input type="hidden" id="conflict-id"/>
|
||||
<div id="conflict-details" class="mb-6"></div>
|
||||
<div class="card p-4 rounded-lg border mb-6" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||
<div class="card p-4 mb-6">
|
||||
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Resolution Options</h3>
|
||||
<div class="space-y-3 mb-6">
|
||||
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);">
|
||||
<input type="radio" name="winner" value="koreader" class="mr-3" required/>
|
||||
<label class="flex items-center gap-3 p-3 rounded-lg cursor-pointer card transition-colors" style="border-color: var(--border);">
|
||||
<input type="radio" name="winner" value="koreader" class="w-4 h-4" required/>
|
||||
<div class="flex-1">
|
||||
<span class="font-semibold" style="color: var(--text-primary);">Keep KOReader Progress</span>
|
||||
<p class="text-xs" style="color: var(--text-secondary);">Use progress from your KOReader device</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);">
|
||||
<input type="radio" name="winner" value="kobo" class="mr-3"/>
|
||||
<label class="flex items-center gap-3 p-3 rounded-lg cursor-pointer card transition-colors" style="border-color: var(--border);">
|
||||
<input type="radio" name="winner" value="kobo" class="w-4 h-4"/>
|
||||
<div class="flex-1">
|
||||
<span class="font-semibold" style="color: var(--text-primary);">Keep Kobo Progress</span>
|
||||
<p class="text-xs" style="color: var(--text-secondary);">Use progress from your Kobo device</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);">
|
||||
<input type="radio" name="winner" value="web" class="mr-3"/>
|
||||
<label class="flex items-center gap-3 p-3 rounded-lg cursor-pointer card transition-colors" style="border-color: var(--border);">
|
||||
<input type="radio" name="winner" value="web" class="w-4 h-4"/>
|
||||
<div class="flex-1">
|
||||
<span class="font-semibold" style="color: var(--text-primary);">Keep Web Progress</span>
|
||||
<p class="text-xs" style="color: var(--text-secondary);">Use progress from the web interface</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);">
|
||||
<input type="radio" name="winner" value="manual" class="mr-3"/>
|
||||
<label class="flex items-center gap-3 p-3 rounded-lg cursor-pointer card transition-colors" style="border-color: var(--border);">
|
||||
<input type="radio" name="winner" value="manual" class="w-4 h-4"/>
|
||||
<div class="flex-1">
|
||||
<span class="font-semibold" style="color: var(--text-primary);">Manual Override</span>
|
||||
<p class="text-xs" style="color: var(--text-secondary);">Specify a custom progress value</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div id="manual-override" class="hidden mb-4 p-4 border rounded-lg" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div id="manual-override" class="hidden mb-4 p-4 rounded-lg" style="background-color: var(--bg-primary); border: 1px solid var(--border);">
|
||||
<h4 class="font-semibold mb-3" style="color: var(--text-primary);">Manual Progress</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Percentage (0-100)</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Percentage (0-100)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="manual-percentage"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.01"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Page Number</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Page Number</label>
|
||||
<input
|
||||
type="number"
|
||||
id="manual-page"
|
||||
min="1"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">EPUB CFI</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">EPUB CFI</label>
|
||||
<input
|
||||
type="text"
|
||||
id="manual-epubcfi"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Chapter</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Chapter</label>
|
||||
<input
|
||||
type="number"
|
||||
id="manual-chapter"
|
||||
min="1"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,20 +232,19 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Resolution Reason (optional)</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Resolution Reason (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="resolution-reason"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
placeholder="e.g., Device was offline, using most recent progress"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="hideResolveModal()" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button>
|
||||
<button type="button" @click="deleteConflict()" class="btn-danger px-4 py-2 rounded-lg">Dismiss</button>
|
||||
<button type="submit" class="btn-primary px-6 py-2 rounded-lg">Resolve Conflict</button>
|
||||
<div class="flex justify-end space-x-3 flex-wrap">
|
||||
<button type="button" @click="hideResolveModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" @click="deleteConflict()" class="btn btn-danger">Dismiss</button>
|
||||
<button type="submit" class="btn btn-primary">Resolve Conflict</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,75 +12,82 @@ templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage stri
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, "/custom-section")
|
||||
<main class="max-w-4xl mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Create Custom Section</h1>
|
||||
<p class="mb-6" style="color: var(--text-secondary)">Build a custom dashboard section by defining filter rules or manually selecting books.</p>
|
||||
<main class="mx-auto container px-4 sm:px-6 lg:px-8 py-8 max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("layers", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Create Custom Section</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Build a custom dashboard section by defining filter rules or manually selecting books.</p>
|
||||
</div>
|
||||
<form id="custom-section-form" class="space-y-6">
|
||||
<!-- Section Details -->
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
||||
<h2 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Section Details</h2>
|
||||
<div class="card p-6">
|
||||
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2" style="color: var(--text-primary)">
|
||||
@Icon("info", "h-5 w-5")
|
||||
Section Details
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name *</label>
|
||||
<label for="section-name" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="section-name"
|
||||
name="name"
|
||||
required
|
||||
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Icon (emoji)</label>
|
||||
<label for="section-icon" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Icon (emoji)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="section-icon"
|
||||
name="icon"
|
||||
maxlength="4"
|
||||
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
placeholder="📚"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label>
|
||||
<label for="section-description" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Description</label>
|
||||
<textarea
|
||||
id="section-description"
|
||||
name="description"
|
||||
rows="2"
|
||||
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Library *</label>
|
||||
<label for="section-library" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Library *</label>
|
||||
<select
|
||||
id="section-library"
|
||||
name="library_id"
|
||||
required
|
||||
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="">Select a library...</option>
|
||||
<option value="">Select a library…</option>
|
||||
for _, lib := range libraries {
|
||||
<option value={ lib.ID }>{ lib.Name }</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filter Rules -->
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Filter Rules</h2>
|
||||
<h2 class="text-lg font-semibold flex items-center gap-2" style="color: var(--text-primary)">
|
||||
@Icon("filter", "h-5 w-5")
|
||||
Filter Rules
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
id="add-rule-btn"
|
||||
class="px-3 py-1 rounded-lg text-sm font-medium"
|
||||
style="background-color: var(--accent);"
|
||||
class="btn btn-primary text-sm"
|
||||
>
|
||||
+ Add Rule
|
||||
@Icon("plus", "h-4 w-4")
|
||||
Add Rule
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">
|
||||
@@ -88,101 +95,101 @@ templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage stri
|
||||
</p>
|
||||
<div id="rules-container" class="space-y-3"></div>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<label class="text-sm font-medium" style="color: var(--text-secondary)">Match:</label>
|
||||
<label for="match-type" class="text-xs uppercase tracking-wide font-medium" style="color: var(--text-secondary)">Match:</label>
|
||||
<select
|
||||
id="match-type"
|
||||
name="match_type"
|
||||
class="px-3 py-1 rounded border"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input w-auto"
|
||||
>
|
||||
<option value="all">ALL rules (AND)</option>
|
||||
<option value="any">ANY rule (OR)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Manual Book Selection -->
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
||||
<h2 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Manual Book Selection</h2>
|
||||
<div class="card p-6">
|
||||
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2" style="color: var(--text-primary)">
|
||||
@Icon("book", "h-5 w-5")
|
||||
Manual Book Selection
|
||||
</h2>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">
|
||||
Add specific books to this section. Use the search to find and select multiple books.
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Search Books</label>
|
||||
<label for="book-search" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Search Books</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
id="book-search"
|
||||
name="book_search"
|
||||
class="flex-1 px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
placeholder="Search by title or author..."
|
||||
class="input flex-1"
|
||||
placeholder="Search by title or author…"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
id="search-books-btn"
|
||||
class="px-4 py-2 rounded-lg font-medium"
|
||||
style="background-color: var(--accent);"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
@Icon("search", "h-4 w-4")
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="search-results"
|
||||
class="hidden mb-4 p-3 rounded-lg max-h-60 overflow-y-auto"
|
||||
class="hidden mb-4 card p-3 max-h-60 overflow-y-auto space-y-2"
|
||||
style="background-color: var(--bg-primary);"
|
||||
></div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Selected Books</label>
|
||||
<label class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Selected Books</label>
|
||||
<div
|
||||
id="selected-books"
|
||||
class="min-h-[60px] p-3 rounded-lg border-2 border-dashed"
|
||||
style="border-color: var(--border); background-color: var(--bg-primary);"
|
||||
class="min-h-[60px] p-3 rounded-xl border-2 border-dashed"
|
||||
style="border-color: var(--border-strong); background-color: var(--bg-primary);"
|
||||
>
|
||||
<p class="text-sm text-center" style="color: var(--text-secondary);">No books selected</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Live Preview -->
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Live Preview</h2>
|
||||
<h2 class="text-lg font-semibold flex items-center gap-2" style="color: var(--text-primary)">
|
||||
@Icon("play", "h-5 w-5")
|
||||
Live Preview
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
id="preview-btn"
|
||||
class="px-4 py-2 rounded-lg font-medium"
|
||||
style="background-color: var(--accent);"
|
||||
class="btn btn-secondary text-sm"
|
||||
>
|
||||
@Icon("refresh", "h-4 w-4")
|
||||
Refresh Preview
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="preview-container"
|
||||
class="p-4 rounded-lg"
|
||||
class="card p-4"
|
||||
style="background-color: var(--bg-primary); min-height: 200px;"
|
||||
>
|
||||
<p class="text-center" style="color: var(--text-secondary);">
|
||||
<p class="text-center text-sm" style="color: var(--text-secondary);">
|
||||
Add filter rules or select books to see a preview of your custom section.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Actions -->
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
id="cancel-btn"
|
||||
class="px-6 py-2 rounded-lg font-medium border hover:opacity-80"
|
||||
style="border-color: var(--border); color: var(--text-primary); background-color: var(--bg-secondary);"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
id="save-section-btn"
|
||||
class="px-6 py-2 rounded-lg font-medium text-white hover:opacity-90"
|
||||
style="background-color: var(--accent);"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
@Icon("save", "h-4 w-4")
|
||||
Save Section
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -37,43 +37,115 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"max-w-4xl mx-auto px-4 py-8\"><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">Create Custom Section</h1><p class=\"mb-6\" style=\"color: var(--text-secondary)\">Build a custom dashboard section by defining filter rules or manually selecting books.</p><form id=\"custom-section-form\" class=\"space-y-6\"><!-- Section Details --><div class=\"p-4 rounded-lg\" style=\"background-color: var(--bg-secondary);\"><h2 class=\"text-xl font-semibold mb-4\" style=\"color: var(--text-primary)\">Section Details</h2><div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Name *</label> <input type=\"text\" id=\"section-name\" name=\"name\" required class=\"w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Icon (emoji)</label> <input type=\"text\" id=\"section-icon\" name=\"icon\" maxlength=\"4\" class=\"w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"📚\"></div></div><div class=\"mt-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea id=\"section-description\" name=\"description\" rows=\"2\" class=\"w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></textarea></div><div class=\"mt-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Library *</label> <select id=\"section-library\" name=\"library_id\" required class=\"w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"\">Select a library...</option> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8 max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("layers", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Create Custom Section</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Build a custom dashboard section by defining filter rules or manually selecting books.</p></div><form id=\"custom-section-form\" class=\"space-y-6\"><div class=\"card p-6\"><h2 class=\"text-lg font-semibold mb-4 flex items-center gap-2\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("info", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Section Details</h2><div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label for=\"section-name\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Name *</label> <input type=\"text\" id=\"section-name\" name=\"name\" required class=\"input\"></div><div><label for=\"section-icon\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Icon (emoji)</label> <input type=\"text\" id=\"section-icon\" name=\"icon\" maxlength=\"4\" class=\"input\" placeholder=\"📚\"></div></div><div class=\"mt-4\"><label for=\"section-description\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea id=\"section-description\" name=\"description\" rows=\"2\" class=\"input\"></textarea></div><div class=\"mt-4\"><label for=\"section-library\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Library *</label> <select id=\"section-library\" name=\"library_id\" required class=\"input\"><option value=\"\">Select a library…</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, lib := range libraries {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<option value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<option value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 68, Col: 31}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 73, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 68, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 73, Col: 44}
|
||||
}
|
||||
_, 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, 5, "</option>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</select></div></div><!-- Filter Rules --><div class=\"p-4 rounded-lg\" style=\"background-color: var(--bg-secondary);\"><div class=\"flex items-center justify-between mb-4\"><h2 class=\"text-xl font-semibold\" style=\"color: var(--text-primary)\">Filter Rules</h2><button type=\"button\" id=\"add-rule-btn\" class=\"px-3 py-1 rounded-lg text-sm font-medium\" style=\"background-color: var(--accent);\">+ Add Rule</button></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Books matching these rules will be automatically added to your section. Use AND for all rules, OR for any rule.</p><div id=\"rules-container\" class=\"space-y-3\"></div><div class=\"mt-4 flex items-center gap-2\"><label class=\"text-sm font-medium\" style=\"color: var(--text-secondary)\">Match:</label> <select id=\"match-type\" name=\"match_type\" class=\"px-3 py-1 rounded border\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"all\">ALL rules (AND)</option> <option value=\"any\">ANY rule (OR)</option></select></div></div><!-- Manual Book Selection --><div class=\"p-4 rounded-lg\" style=\"background-color: var(--bg-secondary);\"><h2 class=\"text-xl font-semibold mb-4\" style=\"color: var(--text-primary)\">Manual Book Selection</h2><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Add specific books to this section. Use the search to find and select multiple books.</p><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Search Books</label><div class=\"flex gap-2\"><input type=\"text\" id=\"book-search\" name=\"book_search\" class=\"flex-1 px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"Search by title or author...\" autocomplete=\"off\"> <button type=\"button\" id=\"search-books-btn\" class=\"px-4 py-2 rounded-lg font-medium\" style=\"background-color: var(--accent);\">Search</button></div></div><div id=\"search-results\" class=\"hidden mb-4 p-3 rounded-lg max-h-60 overflow-y-auto\" style=\"background-color: var(--bg-primary);\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Selected Books</label><div id=\"selected-books\" class=\"min-h-[60px] p-3 rounded-lg border-2 border-dashed\" style=\"border-color: var(--border); background-color: var(--bg-primary);\"><p class=\"text-sm text-center\" style=\"color: var(--text-secondary);\">No books selected</p></div></div></div><!-- Live Preview --><div class=\"p-4 rounded-lg\" style=\"background-color: var(--bg-secondary);\"><div class=\"flex items-center justify-between mb-4\"><h2 class=\"text-xl font-semibold\" style=\"color: var(--text-primary)\">Live Preview</h2><button type=\"button\" id=\"preview-btn\" class=\"px-4 py-2 rounded-lg font-medium\" style=\"background-color: var(--accent);\">Refresh Preview</button></div><div id=\"preview-container\" class=\"p-4 rounded-lg\" style=\"background-color: var(--bg-primary); min-height: 200px;\"><p class=\"text-center\" style=\"color: var(--text-secondary);\">Add filter rules or select books to see a preview of your custom section.</p></div></div><!-- Form Actions --><div class=\"flex justify-end gap-3\"><button type=\"button\" id=\"cancel-btn\" class=\"px-6 py-2 rounded-lg font-medium border hover:opacity-80\" style=\"border-color: var(--border); color: var(--text-primary); background-color: var(--bg-secondary);\">Cancel</button> <button type=\"submit\" id=\"save-section-btn\" class=\"px-6 py-2 rounded-lg font-medium text-white hover:opacity-90\" style=\"background-color: var(--accent);\">Save Section</button></div></form></main>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</select></div></div><div class=\"card p-6\"><div class=\"flex items-center justify-between mb-4\"><h2 class=\"text-lg font-semibold flex items-center gap-2\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("filter", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Filter Rules</h2><button type=\"button\" id=\"add-rule-btn\" class=\"btn btn-primary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "Add Rule</button></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Books matching these rules will be automatically added to your section. Use AND for all rules, OR for any rule.</p><div id=\"rules-container\" class=\"space-y-3\"></div><div class=\"mt-4 flex items-center gap-2\"><label for=\"match-type\" class=\"text-xs uppercase tracking-wide font-medium\" style=\"color: var(--text-secondary)\">Match:</label> <select id=\"match-type\" name=\"match_type\" class=\"input w-auto\"><option value=\"all\">ALL rules (AND)</option> <option value=\"any\">ANY rule (OR)</option></select></div></div><div class=\"card p-6\"><h2 class=\"text-lg font-semibold mb-4 flex items-center gap-2\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("book", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "Manual Book Selection</h2><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Add specific books to this section. Use the search to find and select multiple books.</p><div class=\"mb-4\"><label for=\"book-search\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Search Books</label><div class=\"flex gap-2\"><input type=\"text\" id=\"book-search\" name=\"book_search\" class=\"input flex-1\" placeholder=\"Search by title or author…\" autocomplete=\"off\"> <button type=\"button\" id=\"search-books-btn\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("search", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Search</button></div></div><div id=\"search-results\" class=\"hidden mb-4 card p-3 max-h-60 overflow-y-auto space-y-2\" style=\"background-color: var(--bg-primary);\"></div><div class=\"mb-4\"><label class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Selected Books</label><div id=\"selected-books\" class=\"min-h-[60px] p-3 rounded-xl border-2 border-dashed\" style=\"border-color: var(--border-strong); background-color: var(--bg-primary);\"><p class=\"text-sm text-center\" style=\"color: var(--text-secondary);\">No books selected</p></div></div></div><div class=\"card p-6\"><div class=\"flex items-center justify-between mb-4\"><h2 class=\"text-lg font-semibold flex items-center gap-2\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("play", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Live Preview</h2><button type=\"button\" id=\"preview-btn\" class=\"btn btn-secondary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("refresh", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Refresh Preview</button></div><div id=\"preview-container\" class=\"card p-4\" style=\"background-color: var(--bg-primary); min-height: 200px;\"><p class=\"text-center text-sm\" style=\"color: var(--text-secondary);\">Add filter rules or select books to see a preview of your custom section.</p></div></div><div class=\"flex justify-end gap-3\"><button type=\"button\" id=\"cancel-btn\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" id=\"save-section-btn\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Save Section</button></div></form></main>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -81,7 +153,7 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+77
-89
@@ -18,13 +18,15 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle
|
||||
<body x-data="dashboard" x-init="initDashboard()" class="theme-{ user.Theme }">
|
||||
@Header(user, "/dashboard")
|
||||
@LibrarySwitcher(libData, currentLibraryID, DashboardActions())
|
||||
<!-- Collections Container -->
|
||||
<main id="collections-container" class="w-full px-4 py-8">
|
||||
<main id="collections-container" class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Your Library</h1>
|
||||
<p class="text-sm mt-1" style="color: var(--text-secondary)">Pick up where you left off, or explore something new.</p>
|
||||
</div>
|
||||
for _, section := range sections {
|
||||
@CollectionCarousel(section)
|
||||
}
|
||||
</main>
|
||||
<!-- Dashboard Settings Modal -->
|
||||
@DashboardSettingsModal(allSections, hiddenCollections, itemsPerSection)
|
||||
@ErrorToast(errorMessage)
|
||||
</body>
|
||||
@@ -33,89 +35,79 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle
|
||||
|
||||
templ CollectionCarousel(section handlers.SectionData) {
|
||||
<div
|
||||
class="dashboard-collection mb-8"
|
||||
class="dashboard-collection mb-10"
|
||||
data-collection-id={ section.ID }
|
||||
data-is-system={ section.IsSystem }
|
||||
>
|
||||
<!-- Collection Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-end justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-2xl">{ section.Icon }</span>
|
||||
<span class="grid place-items-center h-9 w-9 rounded-lg text-lg" style="background-color: var(--accent-muted);">{ section.Icon }</span>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">{ section.Title }</h2>
|
||||
<h2 class="text-lg font-bold tracking-tight" style="color: var(--text-primary)">{ section.Title }</h2>
|
||||
if section.Description != "" {
|
||||
<p class="text-sm" style="color: var(--text-secondary)">{ section.Description }</p>
|
||||
<p class="text-xs" style="color: var(--text-secondary)">{ section.Description }</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
if section.ViewAllURL != "" {
|
||||
<a
|
||||
href={ section.ViewAllURL }
|
||||
class="text-sm font-medium hover:underline transition-colors"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium hover:underline transition-colors"
|
||||
style="color: var(--accent);"
|
||||
>
|
||||
View All →
|
||||
View All
|
||||
@Icon("arrow-right", "h-4 w-4")
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<!-- Carousel -->
|
||||
<div class="carousel-container relative group">
|
||||
<button
|
||||
class="carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10
|
||||
w-12 h-full bg-gradient-to-r from-gray-900 to-transparent
|
||||
flex items-center justify-start opacity-0 group-hover:opacity-100
|
||||
transition-opacity duration-200"
|
||||
class="carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10 h-full w-12 flex items-center justify-center opacity-0 pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100 transition-opacity duration-200"
|
||||
data-action="scroll-carousel"
|
||||
data-collection-id={ section.ID }
|
||||
data-direction="-1"
|
||||
aria-label="Scroll left"
|
||||
style="background: linear-gradient(to right, var(--bg-primary), transparent);"
|
||||
>
|
||||
<span class="text-3xl pl-2" style="color: var(--text-primary);">‹</span>
|
||||
@Icon("chevron-left", "h-7 w-7")
|
||||
</button>
|
||||
<div
|
||||
id={ "carousel-track-" + section.ID }
|
||||
class="carousel-track flex gap-4 overflow-x-auto
|
||||
scroll-smooth snap-x snap-mandatory
|
||||
px-12 pb-4"
|
||||
class="carousel-track flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory px-6 pb-2"
|
||||
style="scrollbar-width: none; -ms-overflow-style: none;"
|
||||
>
|
||||
for _, item := range section.Items {
|
||||
@BookCard(item)
|
||||
<div class="flex-shrink-0 w-36 sm:w-40 snap-start">
|
||||
@BookCard(item)
|
||||
</div>
|
||||
}
|
||||
if len(section.Items) == 0 {
|
||||
<div class="text-center py-8 w-full" style="color: var(--text-secondary);">
|
||||
<p>No items in this collection</p>
|
||||
<div class="flex flex-col items-center justify-center text-center py-12 w-full gap-2" style="color: var(--text-secondary);">
|
||||
@Icon("book", "h-8 w-8 opacity-50")
|
||||
<p class="text-sm">No items in this collection</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
class="carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10
|
||||
w-12 h-full bg-gradient-to-l from-gray-900 to-transparent
|
||||
flex items-center justify-end opacity-0 group-hover:opacity-100
|
||||
transition-opacity duration-200"
|
||||
class="carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10 h-full w-12 flex items-center justify-center opacity-0 pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100 transition-opacity duration-200"
|
||||
data-action="scroll-carousel"
|
||||
data-collection-id={ section.ID }
|
||||
data-direction="1"
|
||||
aria-label="Scroll right"
|
||||
style="background: linear-gradient(to left, var(--bg-primary), transparent);"
|
||||
>
|
||||
<span class="text-3xl pr-2" style="color: var(--text-primary);">›</span>
|
||||
@Icon("chevron-right", "h-7 w-7")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ BookCard(item handlers.BookInfo) {
|
||||
<a href={ "/media/" + item.MediaItemID } title={ item.Title }>
|
||||
<div
|
||||
class="book-card flex-shrink-0 w-36 rounded-lg overflow-hidden snap-start cursor-pointer
|
||||
transition-transform duration-200 hover:scale-105"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label={ "View " + item.Title }
|
||||
>
|
||||
<div class="book-card relative w-full h-full rounded-xl overflow-hidden cursor-pointer">
|
||||
<a href={ "/media/" + item.MediaItemID } class="block h-full">
|
||||
<div
|
||||
class="aspect-[2/3] overflow-hidden shadow-lg
|
||||
bg-gradient-to-br from-gray-700 to-gray-900"
|
||||
class="book-card-cover aspect-[2/3] overflow-hidden"
|
||||
style="background-color: color-mix(in srgb, var(--text-primary) 8%, transparent);"
|
||||
>
|
||||
if item.CoverImagePath != "" {
|
||||
<img
|
||||
@@ -133,60 +125,72 @@ templ BookCard(item handlers.BookInfo) {
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div class="book-card-text px-2 py-1 bg-[color-mix(in_srgb,var(--wood-border)_40%,transparent)]">
|
||||
<h3 class="font-semibold text-base line-clamp-2" style="color: var(--text-primary)" title={ item.Title }>
|
||||
<div class="book-card-meta">
|
||||
<h3 class="font-semibold text-sm leading-snug line-clamp-2" style="color: var(--text-primary)" title={ item.Title }>
|
||||
{ item.Title }
|
||||
</h3>
|
||||
if item.Author != "" {
|
||||
<p class="text-sm line-clamp-1" style="color: var(--text-secondary)" title={ item.Author }>
|
||||
<p class="text-xs mt-0.5 line-clamp-1" style="color: var(--text-secondary)" title={ item.Author }>
|
||||
{ item.Author }
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
<div class="book-card-action">
|
||||
if item.HasConflict {
|
||||
<a
|
||||
href={ "/media/" + item.MediaItemID }
|
||||
class="book-card-action-btn"
|
||||
aria-label={ "Resolve progress conflict for " + item.Title }
|
||||
title="Resolve progress conflict"
|
||||
>
|
||||
@Icon("book-open", "h-5 w-5")
|
||||
</a>
|
||||
} else {
|
||||
<a
|
||||
href={ "/readers/" + item.MediaItemID }
|
||||
class="book-card-action-btn"
|
||||
aria-label={ "Read " + item.Title }
|
||||
title="Read"
|
||||
>
|
||||
@Icon("book-open", "h-5 w-5")
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections []string, itemsPerSection int) {
|
||||
<div
|
||||
id="dashboard-settings-modal"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="background-color: rgba(0, 0, 0, 0.7);"
|
||||
class="hidden fixed inset-0 z-[70] flex items-center justify-center p-4"
|
||||
style="background-color: var(--surface-overlay);"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg p-6 w-full max-w-2xl mx-4 shadow-2xl"
|
||||
style="background-color: var(--bg-secondary);"
|
||||
>
|
||||
<div class="card p-6 w-full max-w-2xl shadow-2xl" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Customize Dashboard</h2>
|
||||
<button
|
||||
data-action="close-dashboard-settings"
|
||||
class="p-2 hover:bg-gray-700 rounded transition-colors"
|
||||
>
|
||||
✕
|
||||
<div>
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Customize Dashboard</h2>
|
||||
<p class="text-sm mt-1" style="color: var(--text-secondary)">Drag to reorder, toggle to show or hide.</p>
|
||||
</div>
|
||||
<button data-action="close-dashboard-settings" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary);">
|
||||
Drag to reorder collections, toggle visibility with the switch.
|
||||
</p>
|
||||
<!-- Draggable Collection List -->
|
||||
<div id="collection-list" class="space-y-2 mb-6">
|
||||
for _, section := range sections {
|
||||
<div
|
||||
class="collection-item flex items-center justify-between p-3 rounded border
|
||||
cursor-move select-none"
|
||||
class="collection-item flex items-center justify-between p-3 rounded-xl cursor-move select-none card"
|
||||
data-collection-id={ section.ID }
|
||||
data-is-system={ fmt.Sprintf("%v", section.IsSystem) }
|
||||
draggable="true"
|
||||
style="background-color: var(--bg-primary); border-color: var(--border);"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xl" style="color: var(--text-secondary);">☰</span>
|
||||
@Icon("grip", "h-5 w-5 shrink-0")
|
||||
<span class="text-xl">{ section.Icon }</span>
|
||||
<div>
|
||||
<span class="font-medium" style="color: var(--text-primary);">{ section.Title }</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium" style="color: var(--text-primary)">{ section.Title }</span>
|
||||
if section.IsSystem {
|
||||
<span class="text-xs ml-2 px-2 py-1 rounded" style="background-color: var(--accent);">System</span>
|
||||
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">System</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,8 +199,7 @@ templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections
|
||||
<button
|
||||
data-action="restore-system-collection"
|
||||
data-collection-name={ section.ID }
|
||||
class="text-xs px-3 py-1 rounded border hover:opacity-80 transition-opacity"
|
||||
style="border-color: var(--border); color: var(--text-secondary);"
|
||||
class="btn btn-secondary py-1 px-2.5 text-xs"
|
||||
title="Restore { section.Title } to defaults"
|
||||
>
|
||||
Restore
|
||||
@@ -211,21 +214,17 @@ templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections
|
||||
}
|
||||
/>
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer
|
||||
peer-checked:after:translate-x-full peer-checked:after:border-white
|
||||
after:content-[''] after:absolute after:top-[2px] after:left-[2px]
|
||||
after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all
|
||||
peer-checked:bg-blue-600"
|
||||
class="w-11 h-6 rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all"
|
||||
style="background-color: var(--border-strong);"
|
||||
></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- Items Per Section Slider -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Items per Collection: <span id="items-count-display" class="font-bold">{ itemsPerSection }</span>
|
||||
Items per Collection: <span id="items-count-display" class="font-bold" style="color: var(--text-primary)">{ itemsPerSection }</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
@@ -233,26 +232,15 @@ templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections
|
||||
max="50"
|
||||
step="5"
|
||||
value={ itemsPerSection }
|
||||
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer"
|
||||
style="background-color: var(--border-strong);"
|
||||
data-input-action="update-items-count"
|
||||
target="items-count-display"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
data-action="close-dashboard-settings"
|
||||
class="px-4 py-2 rounded-lg border hover:bg-gray-700 transition-colors"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
data-action="save-dashboard-settings"
|
||||
class="px-4 py-2 rounded-lg text-white font-medium hover:opacity-90 transition-opacity"
|
||||
style="background-color: var(--accent);"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
<button data-action="close-dashboard-settings" class="btn btn-secondary">Cancel</button>
|
||||
<button data-action="save-dashboard-settings" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+283
-170
@@ -46,7 +46,7 @@ func Dashboard(user User, sections []handlers.SectionData, allSections []handler
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Collections Container --><main id=\"collections-container\" class=\"w-full px-4 py-8\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main id=\"collections-container\" class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Your Library</h1><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">Pick up where you left off, or explore something new.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func Dashboard(user User, sections []handlers.SectionData, allSections []handler
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</main><!-- Dashboard Settings Modal -->")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</main>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -97,14 +97,14 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
templ_7745c5c3_Var2 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"dashboard-collection mb-8\" data-collection-id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"dashboard-collection mb-10\" data-collection-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 37, Col: 33}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 39, Col: 33}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -117,33 +117,33 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.IsSystem)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 38, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 40, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><!-- Collection Header --><div class=\"flex items-center justify-between mb-4\"><div class=\"flex items-center gap-3\"><span class=\"text-2xl\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><div class=\"flex items-end justify-between mb-4\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-9 w-9 rounded-lg text-lg\" style=\"background-color: var(--accent-muted);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 43, Col: 41}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 44, Col: 130}
|
||||
}
|
||||
_, 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, 8, "</span><div><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span><div><h2 class=\"text-lg font-bold tracking-tight\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 45, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 46, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -154,14 +154,14 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if section.Description != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<p class=\"text-xs\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(section.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 47, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 48, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -184,73 +184,113 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var8 templ.SafeURL
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(section.ViewAllURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 53, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 54, Col: 30}
|
||||
}
|
||||
_, 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, 14, "\" class=\"text-sm font-medium hover:underline transition-colors\" style=\"color: var(--accent);\">View All →</a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"inline-flex items-center gap-1 text-sm font-medium hover:underline transition-colors\" style=\"color: var(--accent);\">View All")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("arrow-right", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div><!-- Carousel --><div class=\"carousel-container relative group\"><button class=\"carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10 w-12 h-full bg-gradient-to-r from-gray-900 to-transparent flex items-center justify-start opacity-0 group-hover:opacity-100 transition-opacity duration-200\" data-action=\"scroll-carousel\" data-collection-id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"carousel-container relative group\"><button class=\"carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10 h-full w-12 flex items-center justify-center opacity-0 pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100 transition-opacity duration-200\" data-action=\"scroll-carousel\" data-collection-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 69, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 67, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" data-direction=\"-1\" aria-label=\"Scroll left\"><span class=\"text-3xl pl-2\" style=\"color: var(--text-primary);\">‹</span></button><div id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" data-direction=\"-1\" aria-label=\"Scroll left\" style=\"background: linear-gradient(to right, var(--bg-primary), transparent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("chevron-left", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</button><div id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("carousel-track-" + section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 76, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 75, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"carousel-track flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory px-12 pb-4\" style=\"scrollbar-width: none; -ms-overflow-style: none;\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"carousel-track flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory px-6 pb-2\" style=\"scrollbar-width: none; -ms-overflow-style: none;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range section.Items {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"flex-shrink-0 w-36 sm:w-40 snap-start\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = BookCard(item).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if len(section.Items) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"text-center py-8 w-full\" style=\"color: var(--text-secondary);\"><p>No items in this collection</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"flex flex-col items-center justify-center text-center py-12 w-full gap-2\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("book", "h-8 w-8 opacity-50").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p class=\"text-sm\">No items in this collection</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div><button class=\"carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10 w-12 h-full bg-gradient-to-l from-gray-900 to-transparent flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity duration-200\" data-action=\"scroll-carousel\" data-collection-id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div><button class=\"carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10 h-full w-12 flex items-center justify-center opacity-0 pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100 transition-opacity duration-200\" data-action=\"scroll-carousel\" data-collection-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 97, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 94, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" data-direction=\"1\" aria-label=\"Scroll right\"><span class=\"text-3xl pr-2\" style=\"color: var(--text-primary);\">›</span></button></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" data-direction=\"1\" aria-label=\"Scroll right\" style=\"background: linear-gradient(to left, var(--bg-primary), transparent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("chevron-right", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</button></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -279,162 +319,219 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
templ_7745c5c3_Var12 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<div class=\"book-card relative w-full h-full rounded-xl overflow-hidden cursor-pointer\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 templ.SafeURL
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 108, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 107, Col: 40}
|
||||
}
|
||||
_, 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, 22, "\" title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 108, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\"><div class=\"book-card flex-shrink-0 w-36 rounded-lg overflow-hidden snap-start cursor-pointer transition-transform duration-200 hover:scale-105\" tabindex=\"0\" role=\"button\" aria-label=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("View " + item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 114, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"><div class=\"aspect-[2/3] overflow-hidden shadow-lg bg-gradient-to-br from-gray-700 to-gray-900\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" class=\"block h-full\"><div class=\"book-card-cover aspect-[2/3] overflow-hidden\" style=\"background-color: color-mix(in srgb, var(--text-primary) 8%, transparent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.CoverImagePath != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<img src=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 114, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" alt=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 115, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" class=\"w-full h-full object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<img src=\"/static/placeholder-book.svg\" alt=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath)
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 122, Col: 31}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 123, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" alt=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 123, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" class=\"w-full h-full object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<img src=\"/static/placeholder-book.svg\" alt=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 131, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"w-full h-full object-cover\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" class=\"w-full h-full object-cover\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div><div class=\"book-card-text px-2 py-1 bg-[color-mix(in_srgb,var(--wood-border)_40%,transparent)]\"><h3 class=\"font-semibold text-base line-clamp-2\" style=\"color: var(--text-primary)\" title=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</div><div class=\"book-card-meta\"><h3 class=\"font-semibold text-sm leading-snug line-clamp-2\" style=\"color: var(--text-primary)\" title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 137, Col: 106}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 129, Col: 117}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 138, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 130, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
_, 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, 32, "</h3>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.Author != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<p class=\"text-sm line-clamp-1\" style=\"color: var(--text-secondary)\" title=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<p class=\"text-xs mt-0.5 line-clamp-1\" style=\"color: var(--text-secondary)\" title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Author)
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 141, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 133, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 142, Col: 19}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 134, Col: 19}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
_, 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, 35, "</p>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</div></div></a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></a><div class=\"book-card-action\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.HasConflict {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 templ.SafeURL
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 142, Col: 40}
|
||||
}
|
||||
_, 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, 42, "\" class=\"book-card-action-btn\" aria-label=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue("Resolve progress conflict for " + item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 144, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" title=\"Resolve progress conflict\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 templ.SafeURL
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs("/readers/" + item.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 151, Col: 42}
|
||||
}
|
||||
_, 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, 46, "\" class=\"book-card-action-btn\" aria-label=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue("Read " + item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 153, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" title=\"Read\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -458,143 +555,159 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var23 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var23 == nil {
|
||||
templ_7745c5c3_Var23 = templ.NopComponent
|
||||
templ_7745c5c3_Var25 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var25 == nil {
|
||||
templ_7745c5c3_Var25 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div id=\"dashboard-settings-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"rounded-lg p-6 w-full max-w-2xl mx-4 shadow-2xl\" style=\"background-color: var(--bg-secondary);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Customize Dashboard</h2><button data-action=\"close-dashboard-settings\" class=\"p-2 hover:bg-gray-700 rounded transition-colors\">✕</button></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary);\">Drag to reorder collections, toggle visibility with the switch.</p><!-- Draggable Collection List --><div id=\"collection-list\" class=\"space-y-2 mb-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<div id=\"dashboard-settings-modal\" class=\"hidden fixed inset-0 z-[70] flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-2xl shadow-2xl\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><div><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Customize Dashboard</h2><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">Drag to reorder, toggle to show or hide.</p></div><button data-action=\"close-dashboard-settings\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</button></div><div id=\"collection-list\" class=\"space-y-2 mb-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, section := range sections {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"collection-item flex items-center justify-between p-3 rounded border cursor-move select-none\" data-collection-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 178, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" data-is-system=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%v", section.IsSystem))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 179, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" draggable=\"true\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><div class=\"flex items-center gap-3\"><span class=\"text-xl\" style=\"color: var(--text-secondary);\">☰</span> <span class=\"text-xl\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<div class=\"collection-item flex items-center justify-between p-3 rounded-xl cursor-move select-none card\" data-collection-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 185, Col: 43}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 183, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span><div><span class=\"font-medium\" style=\"color: var(--text-primary);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" data-is-system=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%v", section.IsSystem))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 187, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 184, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</span> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" draggable=\"true\"><div class=\"flex items-center gap-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("grip", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<span class=\"text-xl\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 189, Col: 43}
|
||||
}
|
||||
_, 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, 56, "</span><div class=\"flex items-center gap-2\"><span class=\"font-medium\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 191, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if section.IsSystem {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<span class=\"text-xs ml-2 px-2 py-1 rounded\" style=\"background-color: var(--accent);\">System</span>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">System</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</div></div><div class=\"flex items-center gap-3\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div></div><div class=\"flex items-center gap-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if section.IsSystem {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<button data-action=\"restore-system-collection\" data-collection-name=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<button data-action=\"restore-system-collection\" data-collection-name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 197, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 201, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" class=\"text-xs px-3 py-1 rounded border hover:opacity-80 transition-opacity\" style=\"border-color: var(--border); color: var(--text-secondary);\" title=\"Restore { section.Title } to defaults\">Restore</button> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" class=\"btn btn-secondary py-1 px-2.5 text-xs\" title=\"Restore { section.Title } to defaults\">Restore</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<label class=\"relative inline-flex items-center cursor-pointer\"><input type=\"checkbox\" class=\"sr-only peer\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<label class=\"relative inline-flex items-center cursor-pointer\"><input type=\"checkbox\" class=\"sr-only peer\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !ContainsString(hiddenCollections, section.ID) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " checked")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "><div class=\"w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600\"></div></label></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "><div class=\"w-11 h-6 rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all\" style=\"background-color: var(--border-strong);\"></div></label></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div><!-- Items Per Section Slider --><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Items per Collection: <span id=\"items-count-display\" class=\"font-bold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</div><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Items per Collection: <span id=\"items-count-display\" class=\"font-bold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(itemsPerSection)
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(itemsPerSection)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 228, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 227, Col: 128}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span></label> <input type=\"range\" min=\"10\" max=\"50\" step=\"5\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</span></label> <input type=\"range\" min=\"10\" max=\"50\" step=\"5\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(itemsPerSection)
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(itemsPerSection)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 235, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 234, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\" class=\"w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer\" data-input-action=\"update-items-count\" target=\"items-count-display\"></div><div class=\"flex justify-end gap-3\"><button data-action=\"close-dashboard-settings\" class=\"px-4 py-2 rounded-lg border hover:bg-gray-700 transition-colors\" style=\"border-color: var(--border); color: var(--text-primary);\">Cancel</button> <button data-action=\"save-dashboard-settings\" class=\"px-4 py-2 rounded-lg text-white font-medium hover:opacity-90 transition-opacity\" style=\"background-color: var(--accent);\">Save Changes</button></div></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\" class=\"w-full h-2 rounded-lg appearance-none cursor-pointer\" style=\"background-color: var(--border-strong);\" data-input-action=\"update-items-count\" target=\"items-count-display\"></div><div class=\"flex justify-end gap-3\"><button data-action=\"close-dashboard-settings\" class=\"btn btn-secondary\">Cancel</button> <button data-action=\"save-dashboard-settings\" class=\"btn btn-primary\">Save Changes</button></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+177
-156
@@ -14,56 +14,58 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }" x-data="devices" x-init="setupEventDelegation()">
|
||||
@Header(user, "/devices")
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header Section -->
|
||||
<div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex justify-between items-center gap-4 flex-wrap">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Device Management</h1>
|
||||
<p style="color: var(--text-secondary)">Manage your reading devices and sync settings</p>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("device", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Device Management</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Manage your reading devices and sync settings</p>
|
||||
</div>
|
||||
<button @click="showAddDeviceModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||||
➕ Add New Device
|
||||
<button @click="showAddDeviceModal()" class="btn btn-primary">
|
||||
@Icon("plus", "h-4 w-4")
|
||||
Add New Device
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Devices Grid -->
|
||||
<div id="devices-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Empty state -->
|
||||
if len(devices) == 0 {
|
||||
<div id="empty-state" class="text-center py-16 col-span-full" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">📱</div>
|
||||
<div id="empty-state" class="card text-center py-16 col-span-full">
|
||||
<span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("device", "h-7 w-7")
|
||||
</span>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Devices Yet</h3>
|
||||
<p class="mb-4">Add your reading devices to enable cross-device sync</p>
|
||||
<button @click="showAddDeviceModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||||
Add Your First Device
|
||||
</button>
|
||||
<p class="mb-4 text-sm" style="color: var(--text-secondary)">Add your reading devices to enable cross-device sync</p>
|
||||
<button @click="showAddDeviceModal()" class="btn btn-primary">Add Your First Device</button>
|
||||
</div>
|
||||
}
|
||||
<!-- Devices Grid -->
|
||||
if len(devices) > 0 {
|
||||
<div id="devices-grid" class="grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div id="devices-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 col-span-full">
|
||||
for _, device := range devices {
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="card p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="text-4xl">
|
||||
<span class="grid place-items-center h-11 w-11 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
if device.DeviceType == "koreader" {
|
||||
📖
|
||||
@Icon("book-open", "h-5 w-5")
|
||||
} else if device.DeviceType == "kobo" {
|
||||
📚
|
||||
@Icon("library", "h-5 w-5")
|
||||
} else if device.DeviceType == "web" {
|
||||
🌐
|
||||
@Icon("globe", "h-5 w-5")
|
||||
} else {
|
||||
📱
|
||||
@Icon("device", "h-5 w-5")
|
||||
}
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button @click={ "showShelfMappings('" + device.ID.String() + "')" } class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
||||
📚
|
||||
</button>
|
||||
<button @click={ "showDeviceSettings('" + device.ID.String() + "')" } class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
||||
⚙️
|
||||
</span>
|
||||
<div class="flex space-x-1">
|
||||
<button @click={ "showShelfMappings('" + device.ID.String() + "')" } class="icon-btn" title="Shelf mappings" aria-label="Shelf mappings">
|
||||
@Icon("layers", "h-5 w-5")
|
||||
</button>
|
||||
<button @click={ "showDeviceSettings('" + device.ID.String() + "')" } class="icon-btn" title="Device settings" aria-label="Device settings">
|
||||
@Icon("gear", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-1" style="color: var(--text-primary)">{ device.DeviceName }</h3>
|
||||
@@ -72,9 +74,9 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
<div class="flex justify-between">
|
||||
<span style="color: var(--text-secondary)">Sync Status</span>
|
||||
if device.SyncEnabled {
|
||||
<span style="color: var(--accent)">✓ Enabled</span>
|
||||
<span class="badge status-completed">Enabled</span>
|
||||
} else {
|
||||
<span style="color: var(--text-secondary)">✗ Disabled</span>
|
||||
<span class="badge" style="background-color: var(--surface-hover); color: var(--text-secondary);">Disabled</span>
|
||||
}
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
@@ -82,7 +84,7 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
if device.LastSync != nil {
|
||||
<span style="color: var(--text-primary)">{ FormatInTimezone(*device.LastSync, user.Timezone) }</span>
|
||||
} else {
|
||||
<span style="color: var(--text-primary)">Never</span>
|
||||
<span style="color: var(--text-secondary)">Never</span>
|
||||
}
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
@@ -90,15 +92,13 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
if device.LastSeen != nil {
|
||||
<span style="color: var(--text-primary)">{ FormatInTimezone(*device.LastSeen, user.Timezone) }</span>
|
||||
} else {
|
||||
<span style="color: var(--text-primary)">Never</span>
|
||||
<span style="color: var(--text-secondary)">Never</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- NEW: Sync URL & Token Management -->
|
||||
<div class="border-t pt-4" style="border-color: var(--border);">
|
||||
<p class="text-xs font-semibold mb-2" style="color: var(--text-secondary)">DEVICE SYNC CONFIGURATION</p>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide mb-3" style="color: var(--text-secondary)">Device Sync Configuration</p>
|
||||
if device.DeviceType == "kobo" {
|
||||
<!-- Kobo: Copy Full Sync URL -->
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs mb-1" style="color: var(--text-secondary)">Kobo Sync URL</label>
|
||||
<div class="flex space-x-2">
|
||||
@@ -107,22 +107,20 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
id="sync-url-{ device.ID }"
|
||||
readonly
|
||||
value={ baseURL + "/api/sync/kobo/" + device.AuthToken }
|
||||
class="flex-1 px-3 py-2 text-xs rounded border"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input text-xs font-mono"
|
||||
/>
|
||||
<button
|
||||
@click="copyToClipboard(document.getElementById('sync-url-{ device.ID }').value, 'Kobo sync URL')"
|
||||
class="px-3 py-2 text-xs rounded hover:opacity-80"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
class="btn btn-primary text-xs px-3"
|
||||
>
|
||||
📋 Copy
|
||||
@Icon("copy", "h-4 w-4")
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary);">Paste this URL into Kobo's <code class="px-1 py-0.5 rounded" style="background-color: var(--bg-primary);">api_endpoint</code> setting</p>
|
||||
</div>
|
||||
}
|
||||
if device.DeviceType == "koreader" {
|
||||
<!-- KOReader: Copy Auth Token -->
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs mb-1" style="color: var(--text-secondary)">Auth Token (for plugin)</label>
|
||||
<div class="flex space-x-2">
|
||||
@@ -131,43 +129,40 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
id="auth-token-{ device.ID }"
|
||||
readonly
|
||||
value={ device.AuthToken }
|
||||
class="flex-1 px-3 py-2 text-xs rounded border"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border); font-family: monospace;"
|
||||
class="input text-xs font-mono"
|
||||
/>
|
||||
<button
|
||||
@click="copyToClipboard(document.getElementById('auth-token-{ device.ID }').value, 'Auth token')"
|
||||
class="px-3 py-2 text-xs rounded hover:opacity-80"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
class="btn btn-primary text-xs px-3"
|
||||
>
|
||||
📋 Copy
|
||||
@Icon("copy", "h-4 w-4")
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary);">Enter this token in the KOReader plugin settings</p>
|
||||
</div>
|
||||
}
|
||||
<!-- Regenerate Token Button -->
|
||||
<button
|
||||
hx-put={ "/api/devices/" + device.ID.String() + "/regenerate-token" }
|
||||
hx-confirm="⚠️ Old token will immediately stop working. Regenerate anyway?"
|
||||
class="w-full px-3 py-2 text-xs rounded border hover:opacity-80"
|
||||
style="border-color: var(--border); color: var(--text-secondary); background-color: var(--bg-primary);"
|
||||
hx-confirm="Old token will immediately stop working. Regenerate anyway?"
|
||||
class="btn btn-secondary w-full text-xs"
|
||||
>
|
||||
🔄 Regenerate Token
|
||||
@Icon("refresh", "h-4 w-4")
|
||||
Regenerate Token
|
||||
</button>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary);">⚠️ Old token will immediately stop working</p>
|
||||
<p class="text-xs mt-1" style="color: var(--status-warning);">Old token will immediately stop working</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- Pending Registrations -->
|
||||
if len(pendingRegistrations) > 0 {
|
||||
<div id="pending-section" class="mt-12">
|
||||
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Pending Device Registrations</h2>
|
||||
<h2 class="text-lg font-bold tracking-tight mb-4" style="color: var(--text-primary)">Pending Device Registrations</h2>
|
||||
<div class="space-y-4">
|
||||
for _, reg := range pendingRegistrations {
|
||||
<div class="card p-4 rounded-lg border flex items-center justify-between" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="card p-4 flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<h3 class="font-semibold" style="color: var(--text-primary)">{ reg.DeviceName }</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">
|
||||
@@ -175,10 +170,10 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button @click={ "approveDevice('" + reg.RegistrationID + "')" } class="btn-primary px-4 py-2 rounded-lg text-sm">
|
||||
<button @click={ "approveDevice('" + reg.RegistrationID + "')" } class="btn btn-primary text-sm">
|
||||
Approve
|
||||
</button>
|
||||
<button @click={ "rejectDevice('" + reg.RegistrationID + "')" } class="btn-danger px-4 py-2 rounded-lg text-sm">
|
||||
<button @click={ "rejectDevice('" + reg.RegistrationID + "')" } class="btn btn-danger text-sm">
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
@@ -187,92 +182,134 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- Sync Queue Section -->
|
||||
<div id="sync-queue-section" class="mt-12 hidden">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Sync Queue</h2>
|
||||
<button @click="clearSyncQueue()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Clear Queue
|
||||
</button>
|
||||
<h2 class="text-lg font-bold tracking-tight" style="color: var(--text-primary)">Sync Queue</h2>
|
||||
<button @click="clearSyncQueue()" class="btn btn-secondary">Clear Queue</button>
|
||||
</div>
|
||||
<div id="sync-queue" class="space-y-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add Device Modal -->
|
||||
<div id="add-device-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add New Device</h2>
|
||||
<button @click="hideAddDeviceModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<div id="add-device-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" x-data="{ selectedType: '' }" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add New Device</h2>
|
||||
<button @click="hideAddDeviceModal()" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device Type</label>
|
||||
<select
|
||||
id="device-type"
|
||||
x-model="selectedType"
|
||||
required
|
||||
class="input"
|
||||
>
|
||||
<option value="">Select device type...</option>
|
||||
<option value="koreader">KOReader (Kindle, Kobo, PocketBook)</option>
|
||||
<option value="kobo">Kobo E-Reader</option>
|
||||
<!-- <option value="web">Web Browser</option> -->
|
||||
<!-- <option value="mobile">Mobile App</option> -->
|
||||
</select>
|
||||
</div>
|
||||
<div x-show="selectedType === 'koreader'" x-cloak class="space-y-4">
|
||||
<div class="rounded-lg p-4 space-y-4" style="background-color: var(--surface-secondary);">
|
||||
<h3 class="text-sm font-semibold flex items-center gap-2" style="color: var(--text-primary)">
|
||||
@Icon("book-open", "h-4 w-4")
|
||||
KOReader Plugin Setup
|
||||
</h3>
|
||||
<ol class="space-y-3 text-sm" style="color: var(--text-secondary)">
|
||||
<li class="flex gap-3">
|
||||
<span class="flex-shrink-0 grid place-items-center h-5 w-5 rounded-full text-xs font-bold" style="background-color: var(--accent-muted); color: var(--accent);">1</span>
|
||||
<span>Clone the <a href="https://git.linuxhg.com/Bookhoard/bookhoard.koplugin" class="underline" style="color: var(--accent);">Bookhoard plugin</a> to your KOReader <code class="px-1 py-0.5 rounded" style="background-color: var(--bg-primary);">plugins/</code> directory</span>
|
||||
</li>
|
||||
<li class="flex gap-3">
|
||||
<span class="flex-shrink-0 grid place-items-center h-5 w-5 rounded-full text-xs font-bold" style="background-color: var(--accent-muted); color: var(--accent);">2</span>
|
||||
<span>Open KOReader, tap the <strong>wrench icon</strong> at the top</span>
|
||||
</li>
|
||||
<li class="flex gap-3">
|
||||
<span class="flex-shrink-0 grid place-items-center h-5 w-5 rounded-full text-xs font-bold" style="background-color: var(--accent-muted); color: var(--accent);">3</span>
|
||||
<span>Find and tap <strong>Bookhoard sync</strong></span>
|
||||
</li>
|
||||
<li class="flex gap-3">
|
||||
<span class="flex-shrink-0 grid place-items-center h-5 w-5 rounded-full text-xs font-bold" style="background-color: var(--accent-muted); color: var(--accent);">4</span>
|
||||
<span>Tap <strong>Server URL</strong>, enter your server address, then tap <strong>OK</strong>:</span>
|
||||
</li>
|
||||
<li class="ml-8">
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="text-xs px-2 py-1 rounded font-mono flex-1" style="background-color: var(--bg-primary); color: var(--text-primary);">{ baseURL }</code>
|
||||
<button
|
||||
@click={ "copyToClipboard('" + baseURL + "', 'Server URL')" }
|
||||
class="btn btn-secondary text-xs px-3"
|
||||
>
|
||||
@Icon("copy", "h-4 w-4")
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex gap-3">
|
||||
<span class="flex-shrink-0 grid place-items-center h-5 w-5 rounded-full text-xs font-bold" style="background-color: var(--accent-muted); color: var(--accent);">5</span>
|
||||
<span>Return to this page and refresh — you'll see a <strong>pending registration</strong>. Click <strong>Approve</strong> to connect the device.</span>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="rounded-md p-3 text-xs flex items-start gap-2" style="background-color: var(--accent-muted); color: var(--text-secondary);">
|
||||
@Icon("info", "h-4 w-4 flex-shrink-0 mt-0.5")
|
||||
<span>Once approved, reading progress sync and OPDS catalog access are set up automatically.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="button" @click="hideAddDeviceModal()" class="btn btn-primary">Got it</button>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="selectedType === 'kobo'" x-cloak>
|
||||
<form id="add-device-form" @submit="handleAddDevice($event)">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Name</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="device-name"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
placeholder="My Kindle Paperwhite"
|
||||
class="input"
|
||||
placeholder="My Kobo"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Type</label>
|
||||
<select
|
||||
id="device-type"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
>
|
||||
<option value="">Select device type...</option>
|
||||
<option value="koreader">KOReader (Kindle, Kobo, PocketBook)</option>
|
||||
<option value="kobo">Kobo E-Reader</option>
|
||||
<option value="web">Web Browser</option>
|
||||
<option value="mobile">Mobile App</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Identifier</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device Identifier</label>
|
||||
<input
|
||||
type="text"
|
||||
id="device-identifier"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
placeholder="Hardware ID or Serial Number"
|
||||
/>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Enter your device's unique identifier</p>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="hideAddDeviceModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||||
Register Device
|
||||
</button>
|
||||
<button type="button" @click="hideAddDeviceModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Register Device</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Settings Modal -->
|
||||
<div id="device-settings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
</div>
|
||||
<div id="device-settings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Device Settings</h2>
|
||||
<button @click="hideDeviceSettingsModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<button @click="hideDeviceSettingsModal()" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<form id="device-settings-form" @submit="handleSaveDeviceSettings($event)">
|
||||
<input type="hidden" id="settings-device-id"/>
|
||||
<input type="hidden" id="settings-device-type"/>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Name</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="settings-device-name"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
@@ -300,11 +337,10 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Automatically sync changes</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Frequency</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Sync Frequency</label>
|
||||
<select
|
||||
id="settings-sync-frequency"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="1">Every 1 minute</option>
|
||||
<option value="5" selected>Every 5 minutes</option>
|
||||
@@ -314,14 +350,13 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
</select>
|
||||
</div>
|
||||
<div class="border-t pt-6 mb-6" style="border-color: var(--border);">
|
||||
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Collection View Settings</h3>
|
||||
<h3 class="text-base font-semibold mb-1" style="color: var(--text-primary)">Collection View Settings</h3>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">Configure how collections are displayed on this device</p>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Default View Mode</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Default View Mode</label>
|
||||
<select
|
||||
id="settings-view-mode"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="grid">Grid View</option>
|
||||
<option value="list">List View</option>
|
||||
@@ -329,11 +364,10 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sort Collections By</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Sort Collections By</label>
|
||||
<select
|
||||
id="settings-sort-order"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="name">Collection Name</option>
|
||||
<option value="created">Date Created</option>
|
||||
@@ -342,11 +376,10 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Items Per Page</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Items Per Page</label>
|
||||
<select
|
||||
id="settings-items-per-page"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="12">12 items</option>
|
||||
<option value="24" selected>24 items</option>
|
||||
@@ -378,81 +411,73 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Display progress indicators for books</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="hideDeviceSettingsModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" @click="handleRevokeDevice()" class="btn-danger px-4 py-2 rounded-lg">
|
||||
Revoke Device
|
||||
</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||||
Save Settings
|
||||
</button>
|
||||
<div class="flex justify-end space-x-3 flex-wrap">
|
||||
<button type="button" @click="hideDeviceSettingsModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" @click="handleRevokeDevice()" class="btn btn-danger">Revoke Device</button>
|
||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Shelf Mappings Modal -->
|
||||
<div id="shelf-mappings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div id="shelf-mappings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Collection to Shelf Mappings</h2>
|
||||
<button @click="hideShelfMappingsModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<button @click="hideShelfMappingsModal()" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" id="mappings-device-id"/>
|
||||
<div id="shelf-mappings-container" class="space-y-4 mb-6">
|
||||
<p style="color: var(--text-secondary)">Loading mappings...</p>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="hideShelfMappingsModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Close
|
||||
</button>
|
||||
<button type="button" @click="showAddMappingModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||||
➕ Add Mapping
|
||||
<button type="button" @click="hideShelfMappingsModal()" class="btn btn-secondary">Close</button>
|
||||
<button type="button" @click="showAddMappingModal()" class="btn btn-primary">
|
||||
@Icon("plus", "h-4 w-4")
|
||||
Add Mapping
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add/Edit Shelf Mapping Modal -->
|
||||
<div id="add-mapping-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div id="add-mapping-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Collection Mapping</h2>
|
||||
<button @click="hideAddMappingModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<button @click="hideAddMappingModal()" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<form id="mapping-form" @submit="handleSaveMapping($event)">
|
||||
<input type="hidden" id="mapping-device-id"/>
|
||||
<input type="hidden" id="mapping-id"/>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Collection</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Collection</label>
|
||||
<select
|
||||
id="mapping-collection"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="">Select collection...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Shelf Name</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device Shelf Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="mapping-shelf-name"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
placeholder="e.g., Sci-Fi"
|
||||
/>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Name of the shelf on this device</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Direction</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Sync Direction</label>
|
||||
<select
|
||||
id="mapping-sync-direction"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="bidirectional">Bidirectional (sync both ways)</option>
|
||||
<option value="book_to_device">Bookhoard → Device</option>
|
||||
@@ -461,12 +486,8 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="hideAddMappingModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||||
Save Mapping
|
||||
</button>
|
||||
<button type="button" @click="hideAddMappingModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Mapping</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
+201
-51
File diff suppressed because one or more lines are too long
+84
-83
@@ -12,101 +12,102 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="stylesheet" href="/static/highlight-dark.min.css"/>
|
||||
</head>
|
||||
<body x-data="docs" x-init="initializeSearch(); highlightCurrentPage(); initializeCodeCopyButtons()" class={ "theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased" }>
|
||||
<body x-data="docs" x-init="initializeSearch(); highlightCurrentPage(); initializeCodeCopyButtons()" class={ "theme-" + user.Theme + " page-docs font-sans antialiased" }>
|
||||
@Header(user, currentPath)
|
||||
<!-- Mobile Menu Button -->
|
||||
<button
|
||||
class="lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2 text-text-primary hover:bg-background-secondary/80"
|
||||
class="lg:hidden icon-btn fixed top-[4.75rem] left-4 z-50 bg-surface-raised"
|
||||
onclick="toggleSidebar()"
|
||||
aria-label="Toggle menu"
|
||||
aria-label="Toggle documentation menu"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
@Icon("menu", "h-5 w-5")
|
||||
</button>
|
||||
<!-- Search Results Overlay -->
|
||||
<div id="search-results" class="hidden fixed inset-0 bg-black/95 z-50 overflow-y-auto p-8 transition-opacity duration-200 ease-in-out"></div>
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out">
|
||||
<!-- Search -->
|
||||
<div class="p-4 border-b border-border">
|
||||
<input
|
||||
type="text"
|
||||
class="search-input w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent text-sm"
|
||||
placeholder="Search documentation..."
|
||||
id="docs-search"
|
||||
/>
|
||||
</div>
|
||||
<!-- Navigation Sections -->
|
||||
for _, section := range nav.Sections {
|
||||
<div class="nav-section mb-6">
|
||||
<div
|
||||
class="nav-section-title font-semibold text-xs uppercase tracking-wider text-text-secondary px-4 py-3 cursor-pointer select-none flex items-center justify-between"
|
||||
@click="toggleSection($el)"
|
||||
>
|
||||
<span>{ section.Title }</span>
|
||||
<svg class="w-4 h-4 transform transition-transform section-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
<div id="docs-overlay" class="hidden fixed inset-0 z-40 lg:hidden" style="background: var(--surface-overlay);" onclick="toggleSidebar()"></div>
|
||||
<div id="search-results" class="hidden fixed inset-0 z-50 overflow-y-auto p-8" style="background: var(--surface-overlay);"></div>
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex gap-8 py-8">
|
||||
<aside id="docs-sidebar" class="sidebar fixed lg:sticky top-16 lg:top-20 bottom-0 lg:bottom-auto left-0 lg:left-auto z-40 lg:z-10 w-64 shrink-0 lg:h-[calc(100vh-6rem)] overflow-y-auto -translate-x-full lg:translate-x-0 transition-transform duration-300 ease-in-out bg-surface-raised border-r border-line lg:border-r-0">
|
||||
<div class="p-4 border-b border-line">
|
||||
<div class="relative">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" style="color: var(--text-secondary);">
|
||||
@Icon("search", "h-4 w-4")
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input input pl-9"
|
||||
placeholder="Search documentation…"
|
||||
id="docs-search"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
if section.Collapsed {
|
||||
<div class="nav-items hidden" data-collapsed="true">
|
||||
for _, item := range section.Items {
|
||||
<a href={ item.URL } class="nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline">
|
||||
<span class="mr-2">{ item.Icon }</span>{ item.Title }
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<div class="nav-items" data-collapsed="false">
|
||||
for _, item := range section.Items {
|
||||
<a href={ item.URL } class="nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline">
|
||||
<span class="mr-2">{ item.Icon }</span>{ item.Title }
|
||||
</a>
|
||||
for _, section := range nav.Sections {
|
||||
<div class="nav-section border-b border-line">
|
||||
<div
|
||||
class="nav-section-title flex items-center justify-between px-4 py-3 cursor-pointer select-none text-xs uppercase tracking-wider font-semibold transition-colors hover:bg-surface-hover"
|
||||
style="color: var(--text-secondary);"
|
||||
@click="toggleSection($el)"
|
||||
>
|
||||
<span>{ section.Title }</span>
|
||||
@Icon("chevron-down", "h-4 w-4 section-arrow transition-transform")
|
||||
</div>
|
||||
if section.Collapsed {
|
||||
<div class="nav-items hidden" data-collapsed="true">
|
||||
for _, item := range section.Items {
|
||||
<a href={ item.URL } class="nav-item flex items-center gap-2 py-2 pl-8 pr-4 text-sm no-underline transition-colors hover:bg-surface-hover" style="color: var(--text-secondary);">
|
||||
<span class="text-xs" style="color: var(--accent);">{ item.Icon }</span>
|
||||
<span>{ item.Title }</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<div class="nav-items" data-collapsed="false">
|
||||
for _, item := range section.Items {
|
||||
<a href={ item.URL } class="nav-item flex items-center gap-2 py-2 pl-8 pr-4 text-sm no-underline transition-colors hover:bg-surface-hover" style="color: var(--text-secondary);">
|
||||
<span class="text-xs" style="color: var(--accent);">{ item.Icon }</span>
|
||||
<span>{ item.Title }</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- Main Content -->
|
||||
<main class="main-content lg:ml-72 p-8 max-w-4xl mx-auto">
|
||||
<!-- Breadcrumb -->
|
||||
if len(doc.Breadcrumb) > 0 {
|
||||
<nav class="breadcrumb flex gap-2 text-sm text-text-secondary mb-8" aria-label="Breadcrumb">
|
||||
for i, crumb := range doc.Breadcrumb {
|
||||
if i > 0 {
|
||||
<span class="text-text-secondary/50">›</span>
|
||||
}
|
||||
<a href={ crumb.URL } class="text-accent hover:underline no-underline">{ crumb.Title }</a>
|
||||
</aside>
|
||||
<main class="main-content flex-1 min-w-0 max-w-4xl">
|
||||
if len(doc.Breadcrumb) > 0 {
|
||||
<nav class="breadcrumb flex flex-wrap items-center gap-1.5 text-sm mb-8" aria-label="Breadcrumb" style="color: var(--text-secondary);">
|
||||
for i, crumb := range doc.Breadcrumb {
|
||||
if i > 0 {
|
||||
@Icon("chevron-right", "h-4 w-4")
|
||||
}
|
||||
<a href={ crumb.URL } class="no-underline transition-colors hover:underline" style="color: var(--accent);">{ crumb.Title }</a>
|
||||
}
|
||||
</nav>
|
||||
}
|
||||
</nav>
|
||||
}
|
||||
<!-- Title -->
|
||||
<h1 class="text-4xl font-bold mb-8 text-text-primary">{ doc.Title }</h1>
|
||||
<!-- Table of Contents -->
|
||||
if len(doc.TOC) > 0 {
|
||||
<details class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border">
|
||||
<summary class="cursor-pointer hover:opacity-80 transition-opacity">
|
||||
<strong class="text-text-primary font-semibold">On this page</strong>
|
||||
<span class="ml-2 text-text-secondary text-xs">▼</span>
|
||||
</summary>
|
||||
for _, item := range doc.TOC {
|
||||
<a
|
||||
href={ "#" + item.Anchor }
|
||||
class="toc-item block py-1 text-text-secondary hover:text-accent text-sm no-underline"
|
||||
style={ "margin-left: " + fmt.Sprintf("%drem", item.Level) }
|
||||
>
|
||||
{ item.Title }
|
||||
</a>
|
||||
<h1 class="text-4xl font-bold mb-8 tracking-tight" style="color: var(--text-primary);">{ doc.Title }</h1>
|
||||
if len(doc.TOC) > 0 {
|
||||
<details class="toc card p-4 mb-8">
|
||||
<summary class="cursor-pointer list-none [&::-webkit-details-marker]:hidden flex items-center gap-2 font-semibold transition-colors hover:bg-surface-hover -m-4 p-4 rounded-2xl" style="color: var(--text-primary);">
|
||||
<span>On this page</span>
|
||||
@Icon("chevron-down", "h-4 w-4 ml-auto")
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
for _, item := range doc.TOC {
|
||||
<a
|
||||
href={ "#" + item.Anchor }
|
||||
class="toc-item block py-1 text-sm no-underline transition-colors hover:text-brand"
|
||||
style={ "margin-left: " + fmt.Sprintf("%drem", item.Level) + "; color: var(--text-secondary);" }
|
||||
>
|
||||
{ item.Title }
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
</details>
|
||||
}
|
||||
<!-- Content -->
|
||||
<div class="prose prose-invert max-w-none">
|
||||
@UnsafeHTML(doc.Content).ToComponent()
|
||||
<div class="prose prose-invert max-w-none">
|
||||
@UnsafeHTML(doc.Content).ToComponent()
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
+80
-48
@@ -48,7 +48,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 = []any{"theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased"}
|
||||
var templ_7745c5c3_Var3 = []any{"theme-" + user.Theme + " page-docs font-sans antialiased"}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
@@ -74,267 +74,299 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<!-- Mobile Menu Button --><button class=\"lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2 text-text-primary hover:bg-background-secondary/80\" onclick=\"toggleSidebar()\" aria-label=\"Toggle menu\"><svg class=\"w-6 h-6\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 12h16M4 18h16\"></path></svg></button><!-- Search Results Overlay --><div id=\"search-results\" class=\"hidden fixed inset-0 bg-black/95 z-50 overflow-y-auto p-8 transition-opacity duration-200 ease-in-out\"></div><!-- Sidebar --><div class=\"sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out\"><!-- Search --><div class=\"p-4 border-b border-border\"><input type=\"text\" class=\"search-input w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent text-sm\" placeholder=\"Search documentation...\" id=\"docs-search\"></div><!-- Navigation Sections -->")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button class=\"lg:hidden icon-btn fixed top-[4.75rem] left-4 z-50 bg-surface-raised\" onclick=\"toggleSidebar()\" aria-label=\"Toggle documentation menu\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("menu", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</button><div id=\"docs-overlay\" class=\"hidden fixed inset-0 z-40 lg:hidden\" style=\"background: var(--surface-overlay);\" onclick=\"toggleSidebar()\"></div><div id=\"search-results\" class=\"hidden fixed inset-0 z-50 overflow-y-auto p-8\" style=\"background: var(--surface-overlay);\"></div><div class=\"mx-auto max-w-7xl px-4 sm:px-6 lg:px-8\"><div class=\"flex gap-8 py-8\"><aside id=\"docs-sidebar\" class=\"sidebar fixed lg:sticky top-16 lg:top-20 bottom-0 lg:bottom-auto left-0 lg:left-auto z-40 lg:z-10 w-64 shrink-0 lg:h-[calc(100vh-6rem)] overflow-y-auto -translate-x-full lg:translate-x-0 transition-transform duration-300 ease-in-out bg-surface-raised border-r border-line lg:border-r-0\"><div class=\"p-4 border-b border-line\"><div class=\"relative\"><span class=\"absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("search", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span> <input type=\"text\" class=\"search-input input pl-9\" placeholder=\"Search documentation…\" id=\"docs-search\"></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, section := range nav.Sections {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"nav-section mb-6\"><div class=\"nav-section-title font-semibold text-xs uppercase tracking-wider text-text-secondary px-4 py-3 cursor-pointer select-none flex items-center justify-between\" @click=\"toggleSection($el)\"><span>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"nav-section border-b border-line\"><div class=\"nav-section-title flex items-center justify-between px-4 py-3 cursor-pointer select-none text-xs uppercase tracking-wider font-semibold transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\" @click=\"toggleSection($el)\"><span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 47, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 49, 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, 7, "</span> <svg class=\"w-4 h-4 transform transition-transform section-arrow\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path></svg></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("chevron-down", "h-4 w-4 section-arrow transition-transform").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if section.Collapsed {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"nav-items hidden\" data-collapsed=\"true\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"nav-items hidden\" data-collapsed=\"true\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range section.Items {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 templ.SafeURL
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 55, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 55, Col: 29}
|
||||
}
|
||||
_, 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, 10, "\" class=\"nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline\"><span class=\"mr-2\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" class=\"nav-item flex items-center gap-2 py-2 pl-8 pr-4 text-sm no-underline transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\"><span class=\"text-xs\" style=\"color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 56, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 56, Col: 75}
|
||||
}
|
||||
_, 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, 11, "</span>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 56, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 57, Col: 30}
|
||||
}
|
||||
_, 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, 12, "</a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"nav-items\" data-collapsed=\"false\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"nav-items\" data-collapsed=\"false\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range section.Items {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 templ.SafeURL
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 63, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 64, Col: 29}
|
||||
}
|
||||
_, 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, 16, "\" class=\"nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline\"><span class=\"mr-2\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"nav-item flex items-center gap-2 py-2 pl-8 pr-4 text-sm no-underline transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\"><span class=\"text-xs\" style=\"color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 64, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 65, Col: 75}
|
||||
}
|
||||
_, 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, 17, "</span>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 64, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 66, Col: 30}
|
||||
}
|
||||
_, 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, 18, "</a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span></a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div><!-- Main Content --><main class=\"main-content lg:ml-72 p-8 max-w-4xl mx-auto\"><!-- Breadcrumb -->")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</aside><main class=\"main-content flex-1 min-w-0 max-w-4xl\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(doc.Breadcrumb) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<nav class=\"breadcrumb flex gap-2 text-sm text-text-secondary mb-8\" aria-label=\"Breadcrumb\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<nav class=\"breadcrumb flex flex-wrap items-center gap-1.5 text-sm mb-8\" aria-label=\"Breadcrumb\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for i, crumb := range doc.Breadcrumb {
|
||||
if i > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span class=\"text-text-secondary/50\">›</span>")
|
||||
templ_7745c5c3_Err = Icon("chevron-right", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " <a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " <a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 templ.SafeURL
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(crumb.URL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 81, Col: 26}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 81, Col: 28}
|
||||
}
|
||||
_, 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, 25, "\" class=\"text-accent hover:underline no-underline\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" class=\"no-underline transition-colors hover:underline\" style=\"color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(crumb.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 81, Col: 91}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 81, Col: 129}
|
||||
}
|
||||
_, 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, 26, "</a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</nav>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<!-- Title --><h1 class=\"text-4xl font-bold mb-8 text-text-primary\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<h1 class=\"text-4xl font-bold mb-8 tracking-tight\" style=\"color: var(--text-primary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 86, Col: 69}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 85, Col: 104}
|
||||
}
|
||||
_, 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, 29, "</h1><!-- Table of Contents -->")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(doc.TOC) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<details class=\"toc bg-background-secondary p-4 rounded-lg mb-8 border border-border\"><summary class=\"cursor-pointer hover:opacity-80 transition-opacity\"><strong class=\"text-text-primary font-semibold\">On this page</strong> <span class=\"ml-2 text-text-secondary text-xs\">▼</span></summary> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<details class=\"toc card p-4 mb-8\"><summary class=\"cursor-pointer list-none [&::-webkit-details-marker]:hidden flex items-center gap-2 font-semibold transition-colors hover:bg-surface-hover -m-4 p-4 rounded-2xl\" style=\"color: var(--text-primary);\"><span>On this page</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("chevron-down", "h-4 w-4 ml-auto").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</summary><div class=\"mt-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range doc.TOC {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 templ.SafeURL
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs("#" + item.Anchor)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 96, Col: 32}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 95, Col: 35}
|
||||
}
|
||||
_, 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, 32, "\" class=\"toc-item block py-1 text-text-secondary hover:text-accent text-sm no-underline\" style=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" class=\"toc-item block py-1 text-sm no-underline transition-colors hover:text-brand\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("margin-left: " + fmt.Sprintf("%drem", item.Level))
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("margin-left: " + fmt.Sprintf("%drem", item.Level) + "; color: var(--text-secondary);")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 98, Col: 66}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 97, Col: 105}
|
||||
}
|
||||
_, 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, 33, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 100, Col: 20}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 99, Col: 23}
|
||||
}
|
||||
_, 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, 34, "</a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</details>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</div></details>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<!-- Content --><div class=\"prose prose-invert max-w-none\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"prose prose-invert max-w-none\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -342,7 +374,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div></main></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></main></div></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package templates
|
||||
|
||||
// FilterItem renders a single saved filter item for the list
|
||||
templ FilterItem(id string, name string) {
|
||||
<div
|
||||
class="flex items-center justify-between p-2 rounded hover:opacity-80"
|
||||
style="background-color: var(--bg-primary);"
|
||||
class="flex items-center justify-between gap-2 p-2 rounded-lg hover:bg-surface-hover"
|
||||
data-filter-id={ id }
|
||||
>
|
||||
<button
|
||||
data-action="load-filter"
|
||||
@click="loadFilter($event)"
|
||||
class="flex-1 text-left px-2 py-1 rounded"
|
||||
class="flex-1 text-left px-2 py-1 text-sm rounded-lg hover:bg-surface-hover"
|
||||
style="color: var(--text-primary);"
|
||||
>
|
||||
{ name }
|
||||
@@ -18,11 +16,12 @@ templ FilterItem(id string, name string) {
|
||||
<button
|
||||
data-action="delete-filter"
|
||||
@click="deleteFilter($event)"
|
||||
class="p-1 hover:opacity-70 rounded"
|
||||
class="icon-btn shrink-0"
|
||||
style="color: var(--text-secondary);"
|
||||
title="Delete filter"
|
||||
aria-label="Delete filter"
|
||||
>
|
||||
🗑
|
||||
@Icon("trash", "h-4 w-4")
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ package templates
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// FilterItem renders a single saved filter item for the list
|
||||
func FilterItem(id string, name 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
|
||||
@@ -30,33 +29,41 @@ func FilterItem(id string, name string) templ.Component {
|
||||
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 p-2 rounded hover:opacity-80\" style=\"background-color: var(--bg-primary);\" data-filter-id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex items-center justify-between gap-2 p-2 rounded-lg hover:bg-surface-hover\" data-filter-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 8, Col: 21}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 6, Col: 21}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><button data-action=\"load-filter\" @click=\"loadFilter($event)\" class=\"flex-1 text-left px-2 py-1 rounded\" style=\"color: var(--text-primary);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><button data-action=\"load-filter\" @click=\"loadFilter($event)\" class=\"flex-1 text-left px-2 py-1 text-sm rounded-lg hover:bg-surface-hover\" style=\"color: var(--text-primary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 16, Col: 9}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 14, Col: 9}
|
||||
}
|
||||
_, 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, "</button> <button data-action=\"delete-filter\" @click=\"deleteFilter($event)\" class=\"p-1 hover:opacity-70 rounded\" style=\"color: var(--text-secondary);\" title=\"Delete filter\">🗑</button></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</button> <button data-action=\"delete-filter\" @click=\"deleteFilter($event)\" class=\"icon-btn shrink-0\" style=\"color: var(--text-secondary);\" title=\"Delete filter\" aria-label=\"Delete filter\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+209
-512
@@ -1,531 +1,228 @@
|
||||
package templates
|
||||
|
||||
templ Header(user User, currentPath string) {
|
||||
<nav x-data="header" x-init="initializeSearch(); initializeTheme(); loadWoodPaneling(); updateWoodPanelingIndicators(); restoreLibrarySelection(); initializeScanListener()" class="border-b header-nav" style="border-color: var(--border); background-color: var(--bg-secondary);">
|
||||
<div x-data="{}" x-init="console.log('Alpine is working!', $el)"></div>
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- Left: App Title & Navigation -->
|
||||
<div class="flex items-center space-x-6">
|
||||
<a href="/dashboard" class="text-xl font-bold hover:opacity-80 transition-opacity flex items-center gap-2" style="color: var(--text-primary); text-decoration: none;">
|
||||
<span>📚 Bookhoard</span>
|
||||
<!-- Scan spinner -->
|
||||
<svg
|
||||
x-show="scanning"
|
||||
x-transition
|
||||
class="h-5 w-5 animate-spin"
|
||||
style="color: var(--accent);"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span x-show="scanning && scanProgress > 0" x-transition class="text-xs font-normal" style="color: var(--text-secondary);" x-text="scanProgress + '%'"></span>
|
||||
</a>
|
||||
<div class="hidden nav:flex items-center space-x-4">
|
||||
<a href="/dashboard" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Library
|
||||
</a>
|
||||
<a
|
||||
href="/bookshelf"
|
||||
class="text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-secondary); text-decoration: none;"
|
||||
>
|
||||
All Books
|
||||
</a>
|
||||
<a href="/series" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Series
|
||||
</a>
|
||||
<a href="/collections" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Collections
|
||||
</a>
|
||||
<a href="/progress" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Progress
|
||||
</a>
|
||||
<a href="/devices" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Devices
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Center: Search Box -->
|
||||
<div class="flex-1 max-w-2xl mx-8 hidden nav:block">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id="header-search"
|
||||
placeholder="Search your library..."
|
||||
class="w-full px-4 py-2 pl-10 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<svg class="absolute left-3 top-2.5 h-5 w-5" style="color: var(--text-secondary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right: Theme Switcher & User Menu -->
|
||||
<div x-data="{ themeDropdownOpen: false, userMenuOpen: false }" class="hidden nav:flex items-center space-x-4">
|
||||
<!-- Theme Switcher -->
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="themeDropdownOpen = !themeDropdownOpen"
|
||||
type="button"
|
||||
class="p-2 rounded-lg hover:bg-gray-700 transition-colors"
|
||||
style="background-color: var(--bg-primary);"
|
||||
>
|
||||
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
x-show="themeDropdownOpen"
|
||||
@click.outside="themeDropdownOpen = false"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50"
|
||||
style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;"
|
||||
>
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-semibold mb-3" style="color: var(--text-primary)">Select Theme</h3>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
@click="changeTheme('tokyo-night'); themeDropdownOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #7aa2f7;"></span>
|
||||
Tokyo Night
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('dracula'); themeDropdownOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #bd93f9;"></span>
|
||||
Dracula
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('nord'); themeDropdownOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #88c0d0;"></span>
|
||||
Nord
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('solarized-dark'); themeDropdownOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #2aa198;"></span>
|
||||
Solarized Dark
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('monokai'); themeDropdownOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #a6e22e;"></span>
|
||||
Monokai
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('one-dark-pro'); themeDropdownOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #61dafb;"></span>
|
||||
One Dark Pro
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('material-dark'); themeDropdownOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #80cbc4;"></span>
|
||||
Material Dark
|
||||
</button>
|
||||
<div class="border-t pt-2 mt-2" style="border-color: var(--border);">
|
||||
<p class="text-xs mb-2" style="color: var(--text-secondary)">Bookshelf Background</p>
|
||||
<button
|
||||
@click="changeWoodPaneling('none'); themeDropdownOpen = false"
|
||||
type="button"
|
||||
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
data-wood="none"
|
||||
>
|
||||
None
|
||||
</button>
|
||||
<button
|
||||
@click="changeWoodPaneling('wood-light'); themeDropdownOpen = false"
|
||||
type="button"
|
||||
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
data-wood="wood-light"
|
||||
>
|
||||
<span
|
||||
class="inline-block w-4 h-4 rounded mr-2"
|
||||
style="background: url('/static/textures/wood-light.png'); background-size: cover;"
|
||||
></span>
|
||||
Wood Light
|
||||
</button>
|
||||
<button
|
||||
@click="changeWoodPaneling('wood-dark'); themeDropdownOpen = false"
|
||||
type="button"
|
||||
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
data-wood="wood-dark"
|
||||
>
|
||||
<span
|
||||
class="inline-block w-4 h-4 rounded mr-2"
|
||||
style="background: url('/static/textures/wood-dark.png'); background-size: cover;"
|
||||
></span>
|
||||
Wood Dark
|
||||
</button>
|
||||
<button
|
||||
@click="changeWoodPaneling('wood-mahogany'); themeDropdownOpen = false"
|
||||
type="button"
|
||||
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
data-wood="wood-mahogany"
|
||||
>
|
||||
<span
|
||||
class="inline-block w-4 h-4 rounded mr-2"
|
||||
style="background: url('/static/textures/wood-mahogany.png'); background-size: cover;"
|
||||
></span>
|
||||
Wood Mahogany
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- User Icon with Dropdown -->
|
||||
<div class="relative">
|
||||
if user.ID != "" {
|
||||
<!-- LOGGED IN: Show user menu with logout -->
|
||||
<div
|
||||
x-data="header"
|
||||
x-init="initializeSearch(); initializeTheme(); loadWoodPaneling(); updateWoodPanelingIndicators(); restoreLibrarySelection(); initializeScanListener()"
|
||||
>
|
||||
<!-- Mobile backdrop -->
|
||||
<div
|
||||
class="app-sidebar-backdrop lg:hidden"
|
||||
:class="{ 'is-open': mobileMenuOpen }"
|
||||
@click="mobileMenuOpen = false"
|
||||
x-cloak
|
||||
></div>
|
||||
<!-- Sidebar -->
|
||||
<aside class="app-sidebar" :class="{ 'is-open': mobileMenuOpen }">
|
||||
<!-- Logo -->
|
||||
<a
|
||||
href="/dashboard"
|
||||
class="flex items-center gap-2.5 px-5 h-16 shrink-0 border-b"
|
||||
style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);"
|
||||
>
|
||||
<span class="text-xl">📚</span>
|
||||
<span class="font-bold text-lg tracking-tight" style="color: var(--text-primary)">Bookhoard</span>
|
||||
<svg
|
||||
x-show="scanning"
|
||||
x-cloak
|
||||
x-transition
|
||||
class="h-4 w-4 animate-spin ml-auto"
|
||||
style="color: var(--accent);"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<span
|
||||
x-show="scanning && scanProgress > 0"
|
||||
x-cloak
|
||||
x-transition
|
||||
class="block px-5 py-1.5 text-xs font-medium -mt-1"
|
||||
style="color: var(--text-secondary);"
|
||||
x-text="'Scanning… ' + scanProgress + '%'"
|
||||
></span>
|
||||
<!-- Primary navigation -->
|
||||
<nav class="flex-1 px-3 py-4 space-y-0.5 overflow-y-auto">
|
||||
<a href="/dashboard" class={ activeClass(currentPath, "/dashboard") }>
|
||||
@Icon("library", "h-5 w-5 shrink-0")
|
||||
<span>Library</span>
|
||||
</a>
|
||||
<a href="/bookshelf" class={ activeClass(currentPath, "/bookshelf") }>
|
||||
@Icon("book", "h-5 w-5 shrink-0")
|
||||
<span>All Books</span>
|
||||
</a>
|
||||
<a href="/series" class={ activeClass(currentPath, "/series") }>
|
||||
@Icon("layers", "h-5 w-5 shrink-0")
|
||||
<span>Series</span>
|
||||
</a>
|
||||
<a href="/collections" class={ activeClass(currentPath, "/collections") }>
|
||||
@Icon("folder", "h-5 w-5 shrink-0")
|
||||
<span>Collections</span>
|
||||
</a>
|
||||
<a href="/progress" class={ activeClass(currentPath, "/progress") }>
|
||||
@Icon("clock", "h-5 w-5 shrink-0")
|
||||
<span>Progress</span>
|
||||
</a>
|
||||
<a href="/devices" class={ activeClass(currentPath, "/devices") }>
|
||||
@Icon("device", "h-5 w-5 shrink-0")
|
||||
<span>Devices</span>
|
||||
</a>
|
||||
</nav>
|
||||
<!-- Bottom: theme picker + user -->
|
||||
<div
|
||||
class="shrink-0 px-3 py-3 space-y-2 border-t"
|
||||
style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);"
|
||||
>
|
||||
if user.ID != "" {
|
||||
@SidebarUserMenu(user)
|
||||
} else {
|
||||
@SidebarSignIn(currentPath)
|
||||
}
|
||||
<!-- Theme picker -->
|
||||
<div x-data="{ themeOpen: false }" class="sidebar-panel">
|
||||
<button
|
||||
type="button"
|
||||
@click="themeOpen = !themeOpen"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors text-content-muted hover:text-content hover:bg-surface-hover"
|
||||
>
|
||||
@Icon("palette", "h-5 w-5 shrink-0")
|
||||
<span>Appearance</span>
|
||||
@Icon("chevron-down", "h-4 w-4 ml-auto transition-transform")
|
||||
</button>
|
||||
<div x-show="themeOpen" x-cloak x-transition class="mt-1 space-y-0.5 pl-1">
|
||||
for _, opt := range ThemeOptions {
|
||||
<button
|
||||
@click="userMenuOpen = !userMenuOpen"
|
||||
type="button"
|
||||
class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors"
|
||||
style="background-color: var(--bg-primary);"
|
||||
@click={ "changeTheme('" + opt.Name + "'); themeOpen = false" }
|
||||
class="w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover"
|
||||
style="color: var(--text-secondary);"
|
||||
>
|
||||
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
<span class="text-sm hidden sm:block" style="color: var(--text-primary)">{ user.Username }</span>
|
||||
<span class="inline-block w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10" style={ "background-color: " + opt.Color }></span>
|
||||
<span class="flex-1 text-left">{ opt.Label }</span>
|
||||
if user.Theme == opt.Name {
|
||||
@Icon("check", "h-4 w-4 shrink-0")
|
||||
}
|
||||
</button>
|
||||
<div
|
||||
x-show="userMenuOpen"
|
||||
@click.outside="userMenuOpen = false"
|
||||
x-transition
|
||||
class="absolute right-0 mt-2 w-48 rounded-lg shadow-lg z-50"
|
||||
style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;"
|
||||
>
|
||||
<div class="py-1">
|
||||
<a href="/profile" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
||||
Profile
|
||||
</a>
|
||||
if user.Role == "admin" {
|
||||
<a href="/admin" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
||||
Admin Panel
|
||||
</a>
|
||||
}
|
||||
<div class="border-t my-1" style="border-color: var(--border);"></div>
|
||||
<button
|
||||
@click="logout(); userMenuOpen = false"
|
||||
type="button"
|
||||
class="block w-full text-left px-4 py-2 text-sm hover:opacity-80"
|
||||
style="color: var(--text-primary); background-color: var(--bg-secondary);"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
<!-- LOGGED OUT: Show login form -->
|
||||
}
|
||||
<div class="my-1.5 mx-3 border-t" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);"></div>
|
||||
<p class="px-3 pb-1 text-xs font-medium uppercase tracking-wide" style="color: var(--text-secondary);">Bookshelf</p>
|
||||
for _, w := range WoodOptions {
|
||||
<button
|
||||
@click="userMenuOpen = !userMenuOpen"
|
||||
type="button"
|
||||
class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors"
|
||||
style="background-color: var(--bg-primary);"
|
||||
@click={ "changeWoodPaneling('" + w.Name + "')" }
|
||||
class="wood-paneling-btn w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors"
|
||||
data-wood={ w.Name }
|
||||
style="color: var(--text-secondary);"
|
||||
>
|
||||
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
<span class="text-sm hidden sm:block" style="color: var(--text-primary)">Login</span>
|
||||
<span class="inline-block w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10" style="background-color: color-mix(in srgb, var(--text-primary) 12%, transparent);"></span>
|
||||
<span class="flex-1 text-left">{ w.Label }</span>
|
||||
</button>
|
||||
<div
|
||||
x-show="userMenuOpen"
|
||||
@click.outside="userMenuOpen = false"
|
||||
x-transition
|
||||
class="absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50 p-4"
|
||||
style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;"
|
||||
>
|
||||
<form hx-post="/api/auth/login" hx-target="#login-result" hx-swap="innerHTML" class="space-y-3">
|
||||
<input type="hidden" name="redirect" value="{ currentPath }"/>
|
||||
<div>
|
||||
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Email or Username</label>
|
||||
<input type="text" name="login" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Password</label>
|
||||
<input type="password" name="password" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required/>
|
||||
</div>
|
||||
<button type="submit" class="w-full py-2 rounded text-sm" style="background-color: var(--accent); color: white;">
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
<div id="login-result"></div>
|
||||
<div class="border-t my-2" style="border-color: var(--border);"></div>
|
||||
<a href="/register" class="block text-center text-sm hover:opacity-80" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Don't have an account? Sign Up
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="mobileMenuOpen = !mobileMenuOpen"
|
||||
type="button"
|
||||
class="nav:hidden p-2 rounded-lg hover:opacity-80 transition-opacity"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary);"
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
x-show="mobileMenuOpen"
|
||||
@click.outside="mobileMenuOpen = false"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2"
|
||||
class="nav:hidden border-t"
|
||||
style="border-color: var(--border); background-color: var(--bg-secondary); display: none;"
|
||||
x-data="{ mobileThemeOpen: false }"
|
||||
>
|
||||
<div class="px-4 py-3 space-y-1">
|
||||
<div class="relative mb-3">
|
||||
<input
|
||||
type="text"
|
||||
id="header-search-mobile"
|
||||
placeholder="Search your library..."
|
||||
class="w-full px-4 py-2 pl-10 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<svg class="absolute left-3 top-2.5 h-5 w-5" style="color: var(--text-secondary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<a href="/dashboard" class="block px-3 py-2 rounded-lg text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Library
|
||||
</a>
|
||||
<a href="/bookshelf" class="block px-3 py-2 rounded-lg text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
All Books
|
||||
</a>
|
||||
<a href="/series" class="block px-3 py-2 rounded-lg text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Series
|
||||
</a>
|
||||
<a href="/collections" class="block px-3 py-2 rounded-lg text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Collections
|
||||
</a>
|
||||
<a href="/progress" class="block px-3 py-2 rounded-lg text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Progress
|
||||
</a>
|
||||
<a href="/devices" class="block px-3 py-2 rounded-lg text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Devices
|
||||
</a>
|
||||
<div class="border-t my-2" style="border-color: var(--border);"></div>
|
||||
<button
|
||||
@click="mobileThemeOpen = !mobileThemeOpen"
|
||||
type="button"
|
||||
class="flex items-center space-x-2 w-full px-3 py-2 rounded-lg text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-secondary);"
|
||||
>
|
||||
<svg class="h-5 w-5" style="color: var(--text-secondary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path>
|
||||
</svg>
|
||||
<span>Theme</span>
|
||||
<svg class="h-4 w-4 ml-auto transform transition-transform" :class="{ 'rotate-180': mobileThemeOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="mobileThemeOpen" x-transition class="pl-8 pr-2 py-1 space-y-1">
|
||||
<button
|
||||
@click="changeTheme('tokyo-night'); mobileThemeOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #7aa2f7;"></span>
|
||||
Tokyo Night
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('dracula'); mobileThemeOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #bd93f9;"></span>
|
||||
Dracula
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('nord'); mobileThemeOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #88c0d0;"></span>
|
||||
Nord
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('solarized-dark'); mobileThemeOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #2aa198;"></span>
|
||||
Solarized Dark
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('monokai'); mobileThemeOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #a6e22e;"></span>
|
||||
Monokai
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('one-dark-pro'); mobileThemeOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #61dafb;"></span>
|
||||
One Dark Pro
|
||||
</button>
|
||||
<button
|
||||
@click="changeTheme('material-dark'); mobileThemeOpen = false"
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 rounded text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #80cbc4;"></span>
|
||||
Material Dark
|
||||
</button>
|
||||
<div class="border-t pt-2 mt-2" style="border-color: var(--border);">
|
||||
<p class="text-xs mb-2 px-3" style="color: var(--text-secondary)">Bookshelf Background</p>
|
||||
<button
|
||||
@click="changeWoodPaneling('none'); mobileThemeOpen = false"
|
||||
type="button"
|
||||
class="wood-paneling-btn w-full text-left px-3 py-2 rounded text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
data-wood="none"
|
||||
>
|
||||
None
|
||||
</button>
|
||||
<button
|
||||
@click="changeWoodPaneling('wood-light'); mobileThemeOpen = false"
|
||||
type="button"
|
||||
class="wood-paneling-btn w-full text-left px-3 py-2 rounded text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
data-wood="wood-light"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background: url('/static/textures/wood-light.png'); background-size: cover;"></span>
|
||||
Wood Light
|
||||
</button>
|
||||
<button
|
||||
@click="changeWoodPaneling('wood-dark'); mobileThemeOpen = false"
|
||||
type="button"
|
||||
class="wood-paneling-btn w-full text-left px-3 py-2 rounded text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
data-wood="wood-dark"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background: url('/static/textures/wood-dark.png'); background-size: cover;"></span>
|
||||
Wood Dark
|
||||
</button>
|
||||
<button
|
||||
@click="changeWoodPaneling('wood-mahogany'); mobileThemeOpen = false"
|
||||
type="button"
|
||||
class="wood-paneling-btn w-full text-left px-3 py-2 rounded text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-primary);"
|
||||
data-wood="wood-mahogany"
|
||||
>
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background: url('/static/textures/wood-mahogany.png'); background-size: cover;"></span>
|
||||
Wood Mahogany
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t my-2" style="border-color: var(--border);"></div>
|
||||
if user.ID != "" {
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center space-x-2 px-3 py-2">
|
||||
<svg class="h-5 w-5" style="color: var(--text-secondary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
<span class="text-sm" style="color: var(--text-primary)">{ user.Username }</span>
|
||||
</div>
|
||||
<a href="/profile" class="block px-3 py-2 pl-10 rounded-lg text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Profile
|
||||
</a>
|
||||
if user.Role == "admin" {
|
||||
<a href="/admin" class="block px-3 py-2 pl-10 rounded-lg text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Admin Panel
|
||||
</a>
|
||||
}
|
||||
<button
|
||||
@click="logout()"
|
||||
type="button"
|
||||
class="block w-full text-left px-3 py-2 pl-10 rounded-lg text-sm hover:opacity-80 transition-opacity"
|
||||
style="color: var(--text-secondary);"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
} else {
|
||||
<div class="space-y-3">
|
||||
<form hx-post="/api/auth/login" hx-target="#login-result-mobile" hx-swap="innerHTML" class="space-y-3">
|
||||
<input type="hidden" name="redirect" value="{ currentPath }"/>
|
||||
<div>
|
||||
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Email or Username</label>
|
||||
<input type="text" name="login" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Password</label>
|
||||
<input type="password" name="password" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required/>
|
||||
</div>
|
||||
<button type="submit" class="w-full py-2 rounded text-sm" style="background-color: var(--accent); color: white;">
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
<div id="login-result-mobile"></div>
|
||||
<a href="/register" class="block text-center text-sm hover:opacity-80" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Don't have an account? Sign Up
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</aside>
|
||||
<!-- Topbar -->
|
||||
<header class="app-topbar">
|
||||
<div class="flex items-center gap-3 px-4 sm:px-6 h-full">
|
||||
<button
|
||||
type="button"
|
||||
class="app-mobile-only icon-btn"
|
||||
@click="mobileMenuOpen = true"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
@Icon("menu", "h-5 w-5")
|
||||
</button>
|
||||
<div class="relative flex-1 max-w-xl">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" style="color: var(--text-secondary);">
|
||||
@Icon("search", "h-4 w-4")
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="header-search"
|
||||
placeholder="Search your library…"
|
||||
class="input pl-10"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<script type="module" src="/static/main.js"></script>
|
||||
</div>
|
||||
}
|
||||
|
||||
// SidebarUserMenu is the bottom-of-sidebar account control for signed-in users.
|
||||
templ SidebarUserMenu(user User) {
|
||||
<div x-data="{ userOpen: false }" class="sidebar-panel">
|
||||
<button
|
||||
type="button"
|
||||
@click="userOpen = !userOpen"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors hover:bg-surface-hover"
|
||||
style="color: var(--text-primary);"
|
||||
>
|
||||
<span class="grid place-items-center h-7 w-7 rounded-full shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("user", "h-4 w-4")
|
||||
</span>
|
||||
<span class="flex-1 text-left truncate">{ user.Username }</span>
|
||||
@Icon("chevron-down", "h-4 w-4 shrink-0 transition-transform")
|
||||
</button>
|
||||
<div x-show="userOpen" x-cloak x-transition class="mt-1 space-y-0.5 pl-1">
|
||||
<a href="/profile" class="flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover" style="color: var(--text-secondary);">
|
||||
@Icon("user", "h-4 w-4")
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
if user.Role == "admin" {
|
||||
<a href="/admin" class="flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover" style="color: var(--text-secondary);">
|
||||
@Icon("settings", "h-4 w-4")
|
||||
<span>Admin Panel</span>
|
||||
</a>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
@click="logout()"
|
||||
class="w-full flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover"
|
||||
style="color: var(--text-secondary);"
|
||||
>
|
||||
@Icon("logout", "h-4 w-4")
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<script type="module" src="/static/main.js"></script>
|
||||
</div>
|
||||
}
|
||||
|
||||
// SidebarSignIn is an inline sign-in for signed-out visitors, preserving the
|
||||
// original header's inline login (htmx POST) so users can auth from any page.
|
||||
templ SidebarSignIn(currentPath string) {
|
||||
<div x-data="{ signInOpen: false }" class="sidebar-panel">
|
||||
<button
|
||||
type="button"
|
||||
@click="signInOpen = !signInOpen"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors hover:bg-surface-hover"
|
||||
style="color: var(--text-primary);"
|
||||
>
|
||||
<span class="grid place-items-center h-7 w-7 rounded-full shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("user", "h-4 w-4")
|
||||
</span>
|
||||
<span class="flex-1 text-left">Sign in</span>
|
||||
</button>
|
||||
<div x-show="signInOpen" x-cloak x-transition class="mt-1 px-1">
|
||||
<form
|
||||
hx-post="/api/auth/login"
|
||||
hx-target="#login-result"
|
||||
hx-swap="innerHTML"
|
||||
class="space-y-2"
|
||||
>
|
||||
<input type="hidden" name="redirect" value={ currentPath }/>
|
||||
<input type="text" name="login" class="input py-1.5 text-sm" placeholder="Email or username" required/>
|
||||
<input type="password" name="password" class="input py-1.5 text-sm" placeholder="Password" required/>
|
||||
<button type="submit" class="btn btn-primary w-full py-1.5 text-sm">Sign In</button>
|
||||
</form>
|
||||
<div id="login-result"></div>
|
||||
<a href="/register" class="block text-center text-xs mt-2 hover:underline" style="color: var(--text-secondary);">
|
||||
Create an account
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
+447
-43
File diff suppressed because one or more lines are too long
@@ -0,0 +1,175 @@
|
||||
package templates
|
||||
|
||||
// Icon renders an inline SVG from the built-in icon set. All icons share the
|
||||
// same 24x24 viewBox, 1.75 stroke width, and round line caps so the UI has one
|
||||
// consistent visual language (replacing the mixed emoji/SVG iconography).
|
||||
// `extra` is appended to the class list (e.g. "h-5 w-5").
|
||||
templ Icon(name string, extra string) {
|
||||
<svg class={ "reader-icon " + extra } width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
switch name {
|
||||
case "search":
|
||||
<circle cx="11" cy="11" r="7"></circle>
|
||||
<path d="m20 20-3.5-3.5"></path>
|
||||
case "user":
|
||||
<circle cx="12" cy="8" r="4"></circle>
|
||||
<path d="M4 20c0-4 4-6 8-6s8 2 8 6"></path>
|
||||
case "users":
|
||||
<circle cx="9" cy="8" r="3.5"></circle>
|
||||
<path d="M2 20c0-3.5 3-5 7-5s7 1.5 7 5"></path>
|
||||
<path d="M16 4.5a3.5 3.5 0 0 1 0 7"></path>
|
||||
<path d="M22 20c0-3-2-4.5-5-4.5"></path>
|
||||
case "palette":
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<circle cx="8" cy="10" r="1"></circle>
|
||||
<circle cx="12" cy="7.5" r="1"></circle>
|
||||
<circle cx="15.5" cy="10" r="1"></circle>
|
||||
<path d="M12 21a3 3 0 0 0 0-6 2 2 0 0 1 0-4h1"></path>
|
||||
case "menu":
|
||||
<path d="M3 6h18M3 12h18M3 18h18"></path>
|
||||
case "chevron-down":
|
||||
<path d="m6 9 6 6 6-6"></path>
|
||||
case "chevron-up":
|
||||
<path d="m6 15 6-6 6 6"></path>
|
||||
case "chevron-left":
|
||||
<path d="m15 6-6 6 6 6"></path>
|
||||
case "chevron-right":
|
||||
<path d="m9 6 6 6-6 6"></path>
|
||||
case "chevrons-left":
|
||||
<path d="m11 6-6 6 6 6M19 6l-6 6 6 6"></path>
|
||||
case "chevrons-right":
|
||||
<path d="m13 6 6 6-6 6M5 6l6 6-6 6"></path>
|
||||
case "close":
|
||||
<path d="M6 6l12 12M18 6 6 18"></path>
|
||||
case "grip":
|
||||
<circle cx="9" cy="6" r="1"></circle>
|
||||
<circle cx="15" cy="6" r="1"></circle>
|
||||
<circle cx="9" cy="12" r="1"></circle>
|
||||
<circle cx="15" cy="12" r="1"></circle>
|
||||
<circle cx="9" cy="18" r="1"></circle>
|
||||
<circle cx="15" cy="18" r="1"></circle>
|
||||
case "settings":
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"></path>
|
||||
case "gear":
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
case "refresh":
|
||||
<path d="M3 12a9 9 0 0 1 15-6.7L21 8"></path>
|
||||
<path d="M21 3v5h-5"></path>
|
||||
<path d="M21 12a9 9 0 0 1-15 6.7L3 16"></path>
|
||||
<path d="M3 21v-5h5"></path>
|
||||
case "book":
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
|
||||
case "book-open":
|
||||
<path d="M12 7v14"></path>
|
||||
<path d="M3 4h6a3 3 0 0 1 3 3v14a2 2 0 0 0-2-2H3z"></path>
|
||||
<path d="M21 4h-6a3 3 0 0 0-3 3v14a2 2 0 0 1 2-2h7z"></path>
|
||||
case "library":
|
||||
<path d="M3 21h18"></path>
|
||||
<path d="M5 21V7l7-4 7 4v14"></path>
|
||||
case "layers":
|
||||
<path d="m12 3 9 5-9 5-9-5 9-5z"></path>
|
||||
<path d="m3 13 9 5 9-5"></path>
|
||||
case "folder":
|
||||
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
||||
case "chart":
|
||||
<path d="M3 3v18h18"></path>
|
||||
<path d="M7 15v3M12 11v7M17 7v11"></path>
|
||||
case "device":
|
||||
<rect x="7" y="2" width="10" height="20" rx="2"></rect>
|
||||
<path d="M11 18h2"></path>
|
||||
case "trash":
|
||||
<path d="M4 7h16M9 7V4h6v3M6 7l1 13a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-13"></path>
|
||||
case "save":
|
||||
<path d="M5 3h11l3 3v15H5z"></path>
|
||||
<path d="M8 3v6h7V3"></path>
|
||||
<path d="M8 14h8v7H8z"></path>
|
||||
case "filter":
|
||||
<path d="M3 4h18l-7 8v6l-4 2v-8z"></path>
|
||||
case "check":
|
||||
<path d="m4 12 5 5 11-11"></path>
|
||||
case "check-circle":
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<path d="m8 12 3 3 5-6"></path>
|
||||
case "x-circle":
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<path d="m9 9 6 6M15 9l-6 6"></path>
|
||||
case "circle":
|
||||
<circle cx="12" cy="12" r="8"></circle>
|
||||
case "bookmark":
|
||||
<path d="M7 3h10a1 1 0 0 1 1 1v17l-6-4-6 4V4a1 1 0 0 1 1-1z"></path>
|
||||
case "star":
|
||||
<path d="m12 3 2.7 5.5 6 .9-4.3 4.2 1 6L12 17l-5.4 2.6 1-6L3.3 9.4l6-.9z"></path>
|
||||
case "arrow-right":
|
||||
<path d="M5 12h14M13 6l6 6-6 6"></path>
|
||||
case "arrow-left":
|
||||
<path d="M19 12H5M11 6l-6 6 6 6"></path>
|
||||
case "home":
|
||||
<path d="M3 11 12 3l9 8"></path>
|
||||
<path d="M5 10v10h14V10"></path>
|
||||
case "logout":
|
||||
<path d="M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3"></path>
|
||||
<path d="M10 17l5-5-5-5"></path>
|
||||
<path d="M15 12H3"></path>
|
||||
case "plus":
|
||||
<path d="M12 5v14M5 12h14"></path>
|
||||
case "edit":
|
||||
<path d="M4 20h4L18 10l-4-4L4 16z"></path>
|
||||
<path d="m14 6 4 4"></path>
|
||||
case "info":
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<path d="M12 11v5M12 8h.01"></path>
|
||||
case "alert":
|
||||
<path d="M12 3 2 20h20z"></path>
|
||||
<path d="M12 10v4M12 17h.01"></path>
|
||||
case "clock":
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<path d="M12 7v5l3 2"></path>
|
||||
case "sync":
|
||||
<path d="M3 8a9 9 0 0 1 15-3l3 3"></path>
|
||||
<path d="M21 16a9 9 0 0 1-15 3l-3-3"></path>
|
||||
case "download":
|
||||
<path d="M12 3v12M7 10l5 5 5-5"></path>
|
||||
<path d="M5 21h14"></path>
|
||||
case "upload":
|
||||
<path d="M12 21V9M7 14l5-5 5 5"></path>
|
||||
<path d="M5 3h14"></path>
|
||||
case "copy":
|
||||
<rect x="9" y="9" width="12" height="12" rx="2"></rect>
|
||||
<path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1"></path>
|
||||
case "external":
|
||||
<path d="M14 4h6v6"></path>
|
||||
<path d="M20 4 10 14"></path>
|
||||
<path d="M19 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h5"></path>
|
||||
case "list":
|
||||
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"></path>
|
||||
case "grid":
|
||||
<rect x="3" y="3" width="7" height="7" rx="1"></rect>
|
||||
<rect x="14" y="3" width="7" height="7" rx="1"></rect>
|
||||
<rect x="3" y="14" width="7" height="7" rx="1"></rect>
|
||||
<rect x="14" y="14" width="7" height="7" rx="1"></rect>
|
||||
case "play":
|
||||
<path d="M6 4l14 8-14 8z"></path>
|
||||
case "heart":
|
||||
<path d="M12 21s-7-4.5-9.5-9A5 5 0 0 1 12 6a5 5 0 0 1 9.5 6c-2.5 4.5-9.5 9-9.5 9z"></path>
|
||||
case "calendar":
|
||||
<rect x="3" y="4" width="18" height="18" rx="2"></rect>
|
||||
<path d="M3 9h18M8 2v4M16 2v4"></path>
|
||||
case "tag":
|
||||
<path d="M3 11V5a2 2 0 0 1 2-2h6l9 9-8 8-9-9z"></path>
|
||||
<circle cx="7.5" cy="7.5" r="1"></circle>
|
||||
case "globe":
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"></path>
|
||||
case "shield":
|
||||
<path d="M12 3 5 6v6c0 4 3 7 7 9 4-2 7-5 7-9V6z"></path>
|
||||
case "database":
|
||||
<ellipse cx="12" cy="5" rx="8" ry="3"></ellipse>
|
||||
<path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5"></path>
|
||||
<path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"></path>
|
||||
default:
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
}
|
||||
</svg>
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package templates
|
||||
|
||||
//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"
|
||||
|
||||
// Icon renders an inline SVG from the built-in icon set. All icons share the
|
||||
// same 24x24 viewBox, 1.75 stroke width, and round line caps so the UI has one
|
||||
// consistent visual language (replacing the mixed emoji/SVG iconography).
|
||||
// `extra` is appended to the class list (e.g. "h-5 w-5").
|
||||
func Icon(name string, extra 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{"reader-icon " + extra}
|
||||
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 class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
switch name {
|
||||
case "search":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<circle cx=\"11\" cy=\"11\" r=\"7\"></circle> <path d=\"m20 20-3.5-3.5\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "user":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<circle cx=\"12\" cy=\"8\" r=\"4\"></circle> <path d=\"M4 20c0-4 4-6 8-6s8 2 8 6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "users":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<circle cx=\"9\" cy=\"8\" r=\"3.5\"></circle> <path d=\"M2 20c0-3.5 3-5 7-5s7 1.5 7 5\"></path> <path d=\"M16 4.5a3.5 3.5 0 0 1 0 7\"></path> <path d=\"M22 20c0-3-2-4.5-5-4.5\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "palette":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle> <circle cx=\"8\" cy=\"10\" r=\"1\"></circle> <circle cx=\"12\" cy=\"7.5\" r=\"1\"></circle> <circle cx=\"15.5\" cy=\"10\" r=\"1\"></circle> <path d=\"M12 21a3 3 0 0 0 0-6 2 2 0 0 1 0-4h1\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "menu":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<path d=\"M3 6h18M3 12h18M3 18h18\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "chevron-down":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<path d=\"m6 9 6 6 6-6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "chevron-up":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<path d=\"m6 15 6-6 6 6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "chevron-left":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<path d=\"m15 6-6 6 6 6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "chevron-right":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<path d=\"m9 6 6 6-6 6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "chevrons-left":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<path d=\"m11 6-6 6 6 6M19 6l-6 6 6 6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "chevrons-right":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<path d=\"m13 6 6 6-6 6M5 6l6 6-6 6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "close":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<path d=\"M6 6l12 12M18 6 6 18\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "grip":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<circle cx=\"9\" cy=\"6\" r=\"1\"></circle> <circle cx=\"15\" cy=\"6\" r=\"1\"></circle> <circle cx=\"9\" cy=\"12\" r=\"1\"></circle> <circle cx=\"15\" cy=\"12\" r=\"1\"></circle> <circle cx=\"9\" cy=\"18\" r=\"1\"></circle> <circle cx=\"15\" cy=\"18\" r=\"1\"></circle>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "settings":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<circle cx=\"12\" cy=\"12\" r=\"3\"></circle> <path d=\"M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "gear":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<circle cx=\"12\" cy=\"12\" r=\"3\"></circle> <path d=\"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "refresh":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<path d=\"M3 12a9 9 0 0 1 15-6.7L21 8\"></path> <path d=\"M21 3v5h-5\"></path> <path d=\"M21 12a9 9 0 0 1-15 6.7L3 16\"></path> <path d=\"M3 21v-5h5\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "book":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20\"></path> <path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "book-open":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<path d=\"M12 7v14\"></path> <path d=\"M3 4h6a3 3 0 0 1 3 3v14a2 2 0 0 0-2-2H3z\"></path> <path d=\"M21 4h-6a3 3 0 0 0-3 3v14a2 2 0 0 1 2-2h7z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "library":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<path d=\"M3 21h18\"></path> <path d=\"M5 21V7l7-4 7 4v14\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "layers":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<path d=\"m12 3 9 5-9 5-9-5 9-5z\"></path> <path d=\"m3 13 9 5 9-5\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "folder":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<path d=\"M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "chart":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<path d=\"M3 3v18h18\"></path> <path d=\"M7 15v3M12 11v7M17 7v11\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "device":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<rect x=\"7\" y=\"2\" width=\"10\" height=\"20\" rx=\"2\"></rect> <path d=\"M11 18h2\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "trash":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<path d=\"M4 7h16M9 7V4h6v3M6 7l1 13a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-13\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "save":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<path d=\"M5 3h11l3 3v15H5z\"></path> <path d=\"M8 3v6h7V3\"></path> <path d=\"M8 14h8v7H8z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "filter":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<path d=\"M3 4h18l-7 8v6l-4 2v-8z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "check":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<path d=\"m4 12 5 5 11-11\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "check-circle":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle> <path d=\"m8 12 3 3 5-6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "x-circle":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle> <path d=\"m9 9 6 6M15 9l-6 6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "circle":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<circle cx=\"12\" cy=\"12\" r=\"8\"></circle>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "bookmark":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<path d=\"M7 3h10a1 1 0 0 1 1 1v17l-6-4-6 4V4a1 1 0 0 1 1-1z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "star":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<path d=\"m12 3 2.7 5.5 6 .9-4.3 4.2 1 6L12 17l-5.4 2.6 1-6L3.3 9.4l6-.9z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "arrow-right":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<path d=\"M5 12h14M13 6l6 6-6 6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "arrow-left":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<path d=\"M19 12H5M11 6l-6 6 6 6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "home":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<path d=\"M3 11 12 3l9 8\"></path> <path d=\"M5 10v10h14V10\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "logout":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<path d=\"M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3\"></path> <path d=\"M10 17l5-5-5-5\"></path> <path d=\"M15 12H3\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "plus":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<path d=\"M12 5v14M5 12h14\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "edit":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<path d=\"M4 20h4L18 10l-4-4L4 16z\"></path> <path d=\"m14 6 4 4\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "info":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle> <path d=\"M12 11v5M12 8h.01\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "alert":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<path d=\"M12 3 2 20h20z\"></path> <path d=\"M12 10v4M12 17h.01\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "clock":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle> <path d=\"M12 7v5l3 2\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "sync":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<path d=\"M3 8a9 9 0 0 1 15-3l3 3\"></path> <path d=\"M21 16a9 9 0 0 1-15 3l-3-3\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "download":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<path d=\"M12 3v12M7 10l5 5 5-5\"></path> <path d=\"M5 21h14\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "upload":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<path d=\"M12 21V9M7 14l5-5 5 5\"></path> <path d=\"M5 3h14\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "copy":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<rect x=\"9\" y=\"9\" width=\"12\" height=\"12\" rx=\"2\"></rect> <path d=\"M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "external":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<path d=\"M14 4h6v6\"></path> <path d=\"M20 4 10 14\"></path> <path d=\"M19 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h5\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "list":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<path d=\"M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "grid":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"></rect> <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"></rect> <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"></rect> <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"></rect>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "play":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<path d=\"M6 4l14 8-14 8z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "heart":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<path d=\"M12 21s-7-4.5-9.5-9A5 5 0 0 1 12 6a5 5 0 0 1 9.5 6c-2.5 4.5-9.5 9-9.5 9z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "calendar":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\"></rect> <path d=\"M3 9h18M8 2v4M16 2v4\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "tag":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<path d=\"M3 11V5a2 2 0 0 1 2-2h6l9 9-8 8-9-9z\"></path> <circle cx=\"7.5\" cy=\"7.5\" r=\"1\"></circle>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "globe":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle> <path d=\"M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "shield":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<path d=\"M12 3 5 6v6c0 4 3 7 7 9 4-2 7-5 7-9V6z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "database":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<ellipse cx=\"12\" cy=\"5\" rx=\"8\" ry=\"3\"></ellipse> <path d=\"M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5\"></path> <path d=\"M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
default:
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
+71
-83
@@ -12,108 +12,96 @@ templ Index(loggedIn bool) {
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body class="theme-tokyo-night" x-data="index" x-init="initIndexTheme(); checkAuthRedirect()">
|
||||
<!-- Hero Section -->
|
||||
<div class="relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-blue-600 to-purple-700 opacity-10"></div>
|
||||
<div class="relative max-w-7xl mx-auto px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8">
|
||||
<div class="flex justify-between items-start mb-8">
|
||||
<div></div>
|
||||
<select id="theme-select" @change="changeTheme" class="px-3 py-2 border rounded text-sm" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)">
|
||||
<option value="tokyo-night">Tokyo Night</option>
|
||||
<option value="dracula">Dracula</option>
|
||||
<option value="nord">Nord</option>
|
||||
<option value="solarized-dark">Solarized Dark</option>
|
||||
<option value="monokai">Monokai</option>
|
||||
<option value="one-dark-pro">One Dark Pro</option>
|
||||
<option value="material-dark">Material Dark</option>
|
||||
<option value="catppuccin-mocha">Catppuccin Mocha</option>
|
||||
<option value="catppuccin-macchiato">Catppuccin Macchiato</option>
|
||||
<option value="catppuccin-frappe">Catppuccin Frappé</option>
|
||||
<option value="catppuccin-latte">Catppuccin Latte</option>
|
||||
</select>
|
||||
<div class="absolute top-4 right-4 z-10">
|
||||
<select
|
||||
id="theme-select"
|
||||
@change="changeTheme"
|
||||
class="input w-auto py-1.5 pr-8 text-sm cursor-pointer"
|
||||
aria-label="Theme"
|
||||
>
|
||||
for _, opt := range ThemeOptions {
|
||||
<option value={ opt.Name }>{ opt.Label }</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<!-- Hero -->
|
||||
<div class="brand-gradient relative overflow-hidden">
|
||||
<div class="max-w-7xl mx-auto px-4 py-20 sm:px-6 sm:py-28 lg:py-36 lg:px-8 text-center">
|
||||
<div class="inline-flex items-center gap-3 mb-6">
|
||||
<span class="text-5xl">📚</span>
|
||||
<span class="text-4xl sm:text-6xl font-extrabold tracking-tight" style="color: var(--text-primary)">Bookhoard</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl sm:text-6xl lg:text-7xl font-extrabold" style="color: var(--text-primary)">
|
||||
📚 Bookhoard
|
||||
</h1>
|
||||
<p class="mt-6 max-w-2xl mx-auto text-xl" style="color: var(--text-secondary)">
|
||||
Your personal ebook management system. Track reading progress, organize your library, and enjoy beautiful themes.
|
||||
</p>
|
||||
<div class="mt-10">
|
||||
<a href="#auth" class="btn-primary px-8 py-3 rounded-lg font-medium text-lg">
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
<p class="mt-4 max-w-2xl mx-auto text-lg sm:text-xl" style="color: var(--text-secondary)">
|
||||
Your personal ebook management system. Track reading progress, organize your library, and sync across every device.
|
||||
</p>
|
||||
<div class="mt-10 flex items-center justify-center gap-3">
|
||||
if loggedIn {
|
||||
<a href="/dashboard" class="btn btn-primary px-8 py-3 text-base">Go to Library</a>
|
||||
} else {
|
||||
<a href="#auth" class="btn btn-primary px-8 py-3 text-base">Get Started</a>
|
||||
<a href="#features" class="btn btn-secondary px-6 py-3 text-base">Learn more</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Features Section -->
|
||||
<div class="py-16" style="background-color: var(--bg-secondary)">
|
||||
<!-- Features -->
|
||||
<div id="features" class="py-16 sm:py-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl font-extrabold" style="color: var(--text-primary)">Why Bookhoard?</h2>
|
||||
<p class="mt-4 text-lg" style="color: var(--text-secondary)">Discover the features that make ebook management effortless</p>
|
||||
<h2 class="text-3xl font-extrabold tracking-tight" style="color: var(--text-primary)">Why Bookhoard?</h2>
|
||||
<p class="mt-3 text-lg" style="color: var(--text-secondary)">Everything you need to manage your ebook library</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div class="card p-6 rounded-xl border">
|
||||
<div class="text-center">
|
||||
<div class="mx-auto w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center mb-4">
|
||||
<span class="text-white text-2xl">📖</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Reading Progress</h3>
|
||||
<p style="color: var(--text-secondary)">Track your reading progress across all your ebooks with detailed statistics.</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="card p-6">
|
||||
<div class="grid place-items-center h-12 w-12 rounded-xl mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("book-open", "h-6 w-6")
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Reading Progress</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Track progress across all your ebooks with detailed statistics and sync.</p>
|
||||
</div>
|
||||
<div class="card p-6 rounded-xl border">
|
||||
<div class="text-center">
|
||||
<div class="mx-auto w-12 h-12 bg-purple-500 rounded-lg flex items-center justify-center mb-4">
|
||||
<span class="text-white text-2xl">🎨</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Beautiful Themes</h3>
|
||||
<p style="color: var(--text-secondary)">Choose from 11 stunning themes including Tokyo Night, Dracula, and Catppuccin variants.</p>
|
||||
<div class="card p-6">
|
||||
<div class="grid place-items-center h-12 w-12 rounded-xl mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("palette", "h-6 w-6")
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Beautiful Themes</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Choose from 11 stunning themes including Tokyo Night, Dracula, and Catppuccin.</p>
|
||||
</div>
|
||||
<div class="card p-6 rounded-xl border">
|
||||
<div class="text-center">
|
||||
<div class="mx-auto w-12 h-12 bg-green-500 rounded-lg flex items-center justify-center mb-4">
|
||||
<span class="text-white text-2xl">⚡</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Fast & Modern</h3>
|
||||
<p style="color: var(--text-secondary)">Built with Go and HTMX for lightning-fast performance and smooth interactions.</p>
|
||||
<div class="card p-6">
|
||||
<div class="grid place-items-center h-12 w-12 rounded-xl mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("device", "h-6 w-6")
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Cross-Device Sync</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Built with Go and HTMX for fast performance and seamless device syncing.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Auth Section -->
|
||||
<!-- Auth -->
|
||||
if !loggedIn {
|
||||
<div id="auth" class="py-16">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl font-extrabold" style="color: var(--text-primary)">Join Bookhoard Today</h2>
|
||||
<p class="mt-4 text-lg" style="color: var(--text-secondary)">Sign in to start managing your ebook library</p>
|
||||
<div id="auth" class="pb-20 px-4">
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="text-2xl font-extrabold tracking-tight" style="color: var(--text-primary)">Welcome back</h2>
|
||||
<p class="mt-2" style="color: var(--text-secondary)">Sign in to start managing your ebook library</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-8">
|
||||
<div class="card p-8 rounded-xl border">
|
||||
<h3 class="text-2xl font-semibold mb-6 text-center" style="color: var(--text-primary)">Login</h3>
|
||||
<form hx-post="/api/auth/login" hx-target="#auth-result" hx-swap="innerHTML">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email or Username</label>
|
||||
<input type="text" name="login" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Password</label>
|
||||
<input type="password" name="password" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required/>
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-primary py-2 rounded-lg font-medium">
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card p-8">
|
||||
<form hx-post="/api/auth/login" hx-target="#auth-result" hx-swap="innerHTML" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Email or Username</label>
|
||||
<input type="text" name="login" class="input" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Password</label>
|
||||
<input type="password" name="password" class="input" required/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full py-2.5">Sign In</button>
|
||||
</form>
|
||||
<p class="text-center text-sm mt-4" style="color: var(--text-secondary)">
|
||||
No account?
|
||||
<a href="/register" class="font-medium hover:underline" style="color: var(--accent)">Register</a>
|
||||
</p>
|
||||
</div>
|
||||
<div id="auth-result" class="mt-8 text-center"></div>
|
||||
<div id="auth-result" class="mt-4 text-center text-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -29,17 +29,92 @@ func Index(loggedIn bool) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Bookhoard - Home</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night\" x-data=\"index\" x-init=\"initIndexTheme(); checkAuthRedirect()\"><!-- Hero Section --><div class=\"relative overflow-hidden\"><div class=\"absolute inset-0 bg-gradient-to-r from-blue-600 to-purple-700 opacity-10\"></div><div class=\"relative max-w-7xl mx-auto px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8\"><div class=\"flex justify-between items-start mb-8\"><div></div><select id=\"theme-select\" @change=\"changeTheme\" class=\"px-3 py-2 border rounded text-sm\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"tokyo-night\">Tokyo Night</option> <option value=\"dracula\">Dracula</option> <option value=\"nord\">Nord</option> <option value=\"solarized-dark\">Solarized Dark</option> <option value=\"monokai\">Monokai</option> <option value=\"one-dark-pro\">One Dark Pro</option> <option value=\"material-dark\">Material Dark</option> <option value=\"catppuccin-mocha\">Catppuccin Mocha</option> <option value=\"catppuccin-macchiato\">Catppuccin Macchiato</option> <option value=\"catppuccin-frappe\">Catppuccin Frappé</option> <option value=\"catppuccin-latte\">Catppuccin Latte</option></select></div><div class=\"text-center\"><h1 class=\"text-4xl sm:text-6xl lg:text-7xl font-extrabold\" style=\"color: var(--text-primary)\">📚 Bookhoard</h1><p class=\"mt-6 max-w-2xl mx-auto text-xl\" style=\"color: var(--text-secondary)\">Your personal ebook management system. Track reading progress, organize your library, and enjoy beautiful themes.</p><div class=\"mt-10\"><a href=\"#auth\" class=\"btn-primary px-8 py-3 rounded-lg font-medium text-lg\">Get Started</a></div></div></div></div><!-- Features Section --><div class=\"py-16\" style=\"background-color: var(--bg-secondary)\"><div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\"><div class=\"text-center mb-12\"><h2 class=\"text-3xl font-extrabold\" style=\"color: var(--text-primary)\">Why Bookhoard?</h2><p class=\"mt-4 text-lg\" style=\"color: var(--text-secondary)\">Discover the features that make ebook management effortless</p></div><div class=\"grid grid-cols-1 md:grid-cols-3 gap-8\"><div class=\"card p-6 rounded-xl border\"><div class=\"text-center\"><div class=\"mx-auto w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center mb-4\"><span class=\"text-white text-2xl\">📖</span></div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Reading Progress</h3><p style=\"color: var(--text-secondary)\">Track your reading progress across all your ebooks with detailed statistics.</p></div></div><div class=\"card p-6 rounded-xl border\"><div class=\"text-center\"><div class=\"mx-auto w-12 h-12 bg-purple-500 rounded-lg flex items-center justify-center mb-4\"><span class=\"text-white text-2xl\">🎨</span></div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Beautiful Themes</h3><p style=\"color: var(--text-secondary)\">Choose from 11 stunning themes including Tokyo Night, Dracula, and Catppuccin variants.</p></div></div><div class=\"card p-6 rounded-xl border\"><div class=\"text-center\"><div class=\"mx-auto w-12 h-12 bg-green-500 rounded-lg flex items-center justify-center mb-4\"><span class=\"text-white text-2xl\">⚡</span></div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Fast & Modern</h3><p style=\"color: var(--text-secondary)\">Built with Go and HTMX for lightning-fast performance and smooth interactions.</p></div></div></div></div></div><!-- Auth Section -->")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Bookhoard - Home</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night\" x-data=\"index\" x-init=\"initIndexTheme(); checkAuthRedirect()\"><div class=\"absolute top-4 right-4 z-10\"><select id=\"theme-select\" @change=\"changeTheme\" class=\"input w-auto py-1.5 pr-8 text-sm cursor-pointer\" aria-label=\"Theme\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !loggedIn {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div id=\"auth\" class=\"py-16\"><div class=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8\"><div class=\"text-center mb-12\"><h2 class=\"text-3xl font-extrabold\" style=\"color: var(--text-primary)\">Join Bookhoard Today</h2><p class=\"mt-4 text-lg\" style=\"color: var(--text-secondary)\">Sign in to start managing your ebook library</p></div><div class=\"grid grid-cols-1 gap-8\"><div class=\"card p-8 rounded-xl border\"><h3 class=\"text-2xl font-semibold mb-6 text-center\" style=\"color: var(--text-primary)\">Login</h3><form hx-post=\"/api/auth/login\" hx-target=\"#auth-result\" hx-swap=\"innerHTML\"><div class=\"space-y-4\"><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Email or Username</label> <input type=\"text\" name=\"login\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" name=\"password\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><button type=\"submit\" class=\"w-full btn-primary py-2 rounded-lg font-medium\">Sign In</button></div></form></div></div><div id=\"auth-result\" class=\"mt-8 text-center\"></div></div></div>")
|
||||
for _, opt := range ThemeOptions {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<option value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 23, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
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
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 23, Col: 44}
|
||||
}
|
||||
_, 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, 4, "</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</select></div><!-- Hero --><div class=\"brand-gradient relative overflow-hidden\"><div class=\"max-w-7xl mx-auto px-4 py-20 sm:px-6 sm:py-28 lg:py-36 lg:px-8 text-center\"><div class=\"inline-flex items-center gap-3 mb-6\"><span class=\"text-5xl\">📚</span> <span class=\"text-4xl sm:text-6xl font-extrabold tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span></div><p class=\"mt-4 max-w-2xl mx-auto text-lg sm:text-xl\" style=\"color: var(--text-secondary)\">Your personal ebook management system. Track reading progress, organize your library, and sync across every device.</p><div class=\"mt-10 flex items-center justify-center gap-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if loggedIn {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"/dashboard\" class=\"btn btn-primary px-8 py-3 text-base\">Go to Library</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"#auth\" class=\"btn btn-primary px-8 py-3 text-base\">Get Started</a> <a href=\"#features\" class=\"btn btn-secondary px-6 py-3 text-base\">Learn more</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div></div><!-- Features --><div id=\"features\" class=\"py-16 sm:py-20\"><div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\"><div class=\"text-center mb-12\"><h2 class=\"text-3xl font-extrabold tracking-tight\" style=\"color: var(--text-primary)\">Why Bookhoard?</h2><p class=\"mt-3 text-lg\" style=\"color: var(--text-secondary)\">Everything you need to manage your ebook library</p></div><div class=\"grid grid-cols-1 md:grid-cols-3 gap-6\"><div class=\"card p-6\"><div class=\"grid place-items-center h-12 w-12 rounded-xl mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("book-open", "h-6 w-6").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Reading Progress</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Track progress across all your ebooks with detailed statistics and sync.</p></div><div class=\"card p-6\"><div class=\"grid place-items-center h-12 w-12 rounded-xl mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("palette", "h-6 w-6").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Beautiful Themes</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Choose from 11 stunning themes including Tokyo Night, Dracula, and Catppuccin.</p></div><div class=\"card p-6\"><div class=\"grid place-items-center h-12 w-12 rounded-xl mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("device", "h-6 w-6").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Cross-Device Sync</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Built with Go and HTMX for fast performance and seamless device syncing.</p></div></div></div></div><!-- Auth -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !loggedIn {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div id=\"auth\" class=\"pb-20 px-4\"><div class=\"max-w-md mx-auto\"><div class=\"text-center mb-8\"><h2 class=\"text-2xl font-extrabold tracking-tight\" style=\"color: var(--text-primary)\">Welcome back</h2><p class=\"mt-2\" style=\"color: var(--text-secondary)\">Sign in to start managing your ebook library</p></div><div class=\"card p-8\"><form hx-post=\"/api/auth/login\" hx-target=\"#auth-result\" hx-swap=\"innerHTML\" class=\"space-y-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Email or Username</label> <input type=\"text\" name=\"login\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" name=\"password\" class=\"input\" required></div><button type=\"submit\" class=\"btn btn-primary w-full py-2.5\">Sign In</button></form><p class=\"text-center text-sm mt-4\" style=\"color: var(--text-secondary)\">No account? <a href=\"/register\" class=\"font-medium hover:underline\" style=\"color: var(--accent)\">Register</a></p></div><div id=\"auth-result\" class=\"mt-4 text-center text-sm\"></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
package templates
|
||||
|
||||
templ LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...templ.Component) {
|
||||
<div class="sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b" style="background-color: var(--bg-primary);">
|
||||
<div class="w-full px-4 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="text-sm font-medium" style="color: var(--text-secondary)">Library:</label>
|
||||
<div
|
||||
class="app-subbar backdrop-blur border-b"
|
||||
style="background-color: color-mix(in srgb, var(--bg-primary) 88%, transparent); border-color: color-mix(in srgb, var(--text-primary) 6%, transparent);"
|
||||
>
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-3 flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-sm font-medium" style="color: var(--text-secondary)">Library</label>
|
||||
<select
|
||||
id="library-select"
|
||||
name="library_id"
|
||||
class="px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-primary);"
|
||||
class="input w-auto py-1.5 pr-8 cursor-pointer"
|
||||
>
|
||||
<option value="">All Libraries ({ TotalMediaCount(libData) })</option>
|
||||
for _, lib := range libData {
|
||||
if lib.ID == currentLibraryID {
|
||||
<option value={ lib.ID } selected>{ lib.Name } ({ lib.MediaCount })</option>
|
||||
} else {
|
||||
<option value={ lib.ID }>{ lib.Name } ({ lib.MediaCount })</option>
|
||||
<option value="">All Libraries ({ TotalMediaCount(libData) })</option>
|
||||
for _, lib := range libData {
|
||||
if lib.ID == currentLibraryID {
|
||||
<option value={ lib.ID } selected>{ lib.Name } ({ lib.MediaCount })</option>
|
||||
} else {
|
||||
<option value={ lib.ID }>{ lib.Name } ({ lib.MediaCount })</option>
|
||||
}
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
if len(actions) > 0 {
|
||||
@@ -42,18 +44,16 @@ templ LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ..
|
||||
templ DashboardActions() {
|
||||
<button
|
||||
data-action="open-dashboard-settings"
|
||||
class="p-2 rounded-lg hover:bg-gray-700 transition-colors"
|
||||
style="background-color: var(--bg-secondary);"
|
||||
class="icon-btn"
|
||||
title="Customize Dashboard"
|
||||
>
|
||||
⚙️
|
||||
@Icon("grid", "h-5 w-5")
|
||||
</button>
|
||||
<button
|
||||
data-action="reload-page"
|
||||
class="p-2 rounded-lg hover:bg-gray-700 transition-colors"
|
||||
style="background-color: var(--bg-secondary);"
|
||||
class="icon-btn"
|
||||
title="Refresh"
|
||||
>
|
||||
🔄
|
||||
@Icon("refresh", "h-5 w-5")
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -29,14 +29,14 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b\" style=\"background-color: var(--bg-primary);\"><div class=\"w-full px-4 py-3 flex items-center justify-between\"><div class=\"flex items-center gap-4\"><label class=\"text-sm font-medium\" style=\"color: var(--text-secondary)\">Library:</label> <select id=\"library-select\" name=\"library_id\" class=\"px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-secondary); color: var(--text-primary);\"><option value=\"\">All Libraries (")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"app-subbar backdrop-blur border-b\" style=\"background-color: color-mix(in srgb, var(--bg-primary) 88%, transparent); border-color: color-mix(in srgb, var(--text-primary) 6%, transparent);\"><div class=\"w-full px-4 sm:px-6 lg:px-8 py-3 flex items-center justify-between gap-4\"><div class=\"flex items-center gap-3\"><label class=\"text-sm font-medium\" style=\"color: var(--text-secondary)\">Library</label> <select id=\"library-select\" name=\"library_id\" class=\"input w-auto py-1.5 pr-8 cursor-pointer\"><option value=\"\">All Libraries (")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libData))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 14, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 16, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -55,7 +55,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 17, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -68,7 +68,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 17, Col: 50}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -81,7 +81,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 17, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 71}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -99,7 +99,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 21, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -112,7 +112,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 41}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 21, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -125,7 +125,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 21, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -186,7 +186,23 @@ func DashboardActions() templ.Component {
|
||||
templ_7745c5c3_Var9 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<button data-action=\"open-dashboard-settings\" class=\"p-2 rounded-lg hover:bg-gray-700 transition-colors\" style=\"background-color: var(--bg-secondary);\" title=\"Customize Dashboard\">⚙️</button> <button data-action=\"reload-page\" class=\"p-2 rounded-lg hover:bg-gray-700 transition-colors\" style=\"background-color: var(--bg-secondary);\" title=\"Refresh\">🔄</button>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<button data-action=\"open-dashboard-settings\" class=\"icon-btn\" title=\"Customize Dashboard\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("grid", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</button> <button data-action=\"reload-page\" class=\"icon-btn\" title=\"Refresh\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("refresh", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+45
-48
@@ -6,60 +6,57 @@ templ Login(sessionExpired bool, deleted bool) {
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Login</title>
|
||||
<title>Login - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script type="module" src="/static/main.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body class="theme-tokyo-night min-h-screen" x-data="login" x-init="initLoginTheme()">
|
||||
<div class="container mx-auto px-4 py-8 max-w-md">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h1 class="text-3xl font-bold">Login to Bookhoard</h1>
|
||||
<select id="theme-select" @change="changeTheme()" class="px-3 py-2 border rounded" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)">
|
||||
<option value="tokyo-night">Tokyo Night</option>
|
||||
<option value="dracula">Dracula</option>
|
||||
<option value="nord">Nord</option>
|
||||
<option value="solarized-dark">Solarized Dark</option>
|
||||
<option value="monokai">Monokai</option>
|
||||
<option value="one-dark-pro">One Dark Pro</option>
|
||||
<option value="material-dark">Material Dark</option>
|
||||
<option value="catppuccin-mocha">Catppuccin Mocha</option>
|
||||
<option value="catppuccin-macchiato">Catppuccin Macchiato</option>
|
||||
<option value="catppuccin-frappe">Catppuccin Frappé</option>
|
||||
<option value="catppuccin-latte">Catppuccin Latte</option>
|
||||
</select>
|
||||
<div class="brand-gradient min-h-screen flex flex-col items-center justify-center p-4 relative">
|
||||
<select
|
||||
id="theme-select"
|
||||
@change="changeTheme()"
|
||||
class="input absolute top-4 right-4 w-auto py-1.5 pr-8 text-sm cursor-pointer"
|
||||
aria-label="Theme"
|
||||
>
|
||||
for _, opt := range ThemeOptions {
|
||||
<option value={ opt.Name }>{ opt.Label }</option>
|
||||
}
|
||||
</select>
|
||||
<div class="w-full max-w-sm">
|
||||
<a href="/" class="flex items-center justify-center gap-2.5 mb-8">
|
||||
<span class="text-3xl">📚</span>
|
||||
<span class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Bookhoard</span>
|
||||
</a>
|
||||
if sessionExpired {
|
||||
<div class="mb-4 p-3 rounded-xl flex items-start gap-2" style="background-color: color-mix(in srgb, var(--accent) 14%, transparent); border: 1px solid var(--accent);">
|
||||
@Icon("info", "h-5 w-5 shrink-0")
|
||||
<p class="text-sm" style="color: var(--text-primary)">Your session has expired. Please log in again to continue.</p>
|
||||
</div>
|
||||
}
|
||||
if deleted {
|
||||
<div class="mb-4 p-3 rounded-xl flex items-start gap-2" style="background-color: color-mix(in srgb, var(--status-success) 14%, transparent); border: 1px solid var(--status-success);">
|
||||
@Icon("check-circle", "h-5 w-5 shrink-0")
|
||||
<p class="text-sm" style="color: var(--text-primary)">Your account has been deleted successfully.</p>
|
||||
</div>
|
||||
}
|
||||
<form hx-post="/api/auth/login" hx-target="#result" hx-swap="innerHTML" class="card p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Email or Username</label>
|
||||
<input type="text" name="login" class="input" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Password</label>
|
||||
<input type="password" name="password" class="input" required/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full py-2.5">Sign In</button>
|
||||
</form>
|
||||
<div id="result" class="mt-4 text-center text-sm"></div>
|
||||
<p class="text-center mt-6 text-sm" style="color: var(--text-secondary)">
|
||||
Don't have an account?
|
||||
<a href="/register" class="font-medium hover:underline" style="color: var(--accent)">Sign up</a>
|
||||
</p>
|
||||
</div>
|
||||
if sessionExpired {
|
||||
<div class="mb-4 p-3 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--accent);">
|
||||
<p style="color: var(--text-primary);">
|
||||
Your session has expired. Please log in again to continue.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
if deleted {
|
||||
<div class="mb-4 p-3 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--accent);">
|
||||
<p style="color: var(--text-primary);">
|
||||
Your account has been deleted successfully.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
<form hx-post="/api/auth/login" hx-target="#result" hx-swap="innerHTML" class="card p-6 rounded-lg shadow-md border">
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2" style="color: var(--text-secondary)">Email or Username</label>
|
||||
<input type="text" name="login" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2" style="color: var(--text-secondary)">Password</label>
|
||||
<input type="password" name="password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required/>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary w-full py-2 rounded">
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
<div id="result" class="mt-4 text-center"></div>
|
||||
<p class="text-center mt-4">
|
||||
<a href="/register" class="text-blue-500 hover:underline">Don't have an account? Sign Up</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -29,23 +29,75 @@ func Login(sessionExpired bool, deleted bool) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Login</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night min-h-screen\" x-data=\"login\" x-init=\"initLoginTheme()\"><div class=\"container mx-auto px-4 py-8 max-w-md\"><div class=\"flex justify-between items-center mb-8\"><h1 class=\"text-3xl font-bold\">Login to Bookhoard</h1><select id=\"theme-select\" @change=\"changeTheme()\" class=\"px-3 py-2 border rounded\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"tokyo-night\">Tokyo Night</option> <option value=\"dracula\">Dracula</option> <option value=\"nord\">Nord</option> <option value=\"solarized-dark\">Solarized Dark</option> <option value=\"monokai\">Monokai</option> <option value=\"one-dark-pro\">One Dark Pro</option> <option value=\"material-dark\">Material Dark</option> <option value=\"catppuccin-mocha\">Catppuccin Mocha</option> <option value=\"catppuccin-macchiato\">Catppuccin Macchiato</option> <option value=\"catppuccin-frappe\">Catppuccin Frappé</option> <option value=\"catppuccin-latte\">Catppuccin Latte</option></select></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Login - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night min-h-screen\" x-data=\"login\" x-init=\"initLoginTheme()\"><div class=\"brand-gradient min-h-screen flex flex-col items-center justify-center p-4 relative\"><select id=\"theme-select\" @change=\"changeTheme()\" class=\"input absolute top-4 right-4 w-auto py-1.5 pr-8 text-sm cursor-pointer\" aria-label=\"Theme\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, opt := range ThemeOptions {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<option value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 23, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
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
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 23, Col: 44}
|
||||
}
|
||||
_, 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, 4, "</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</select><div class=\"w-full max-w-sm\"><a href=\"/\" class=\"flex items-center justify-center gap-2.5 mb-8\"><span class=\"text-3xl\">📚</span> <span class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if sessionExpired {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"mb-4 p-3 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--accent);\"><p style=\"color: var(--text-primary);\">Your session has expired. Please log in again to continue.</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"mb-4 p-3 rounded-xl flex items-start gap-2\" style=\"background-color: color-mix(in srgb, var(--accent) 14%, transparent); border: 1px solid var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("info", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"text-sm\" style=\"color: var(--text-primary)\">Your session has expired. Please log in again to continue.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if deleted {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"mb-4 p-3 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--accent);\"><p style=\"color: var(--text-primary);\">Your account has been deleted successfully.</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"mb-4 p-3 rounded-xl flex items-start gap-2\" style=\"background-color: color-mix(in srgb, var(--status-success) 14%, transparent); border: 1px solid var(--status-success);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("check-circle", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"text-sm\" style=\"color: var(--text-primary)\">Your account has been deleted successfully.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form hx-post=\"/api/auth/login\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"card p-6 rounded-lg shadow-md border\"><div class=\"mb-4\"><label class=\"block mb-2\" style=\"color: var(--text-secondary)\">Email or Username</label> <input type=\"text\" name=\"login\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"mb-4\"><label class=\"block mb-2\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" name=\"password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><button type=\"submit\" class=\"btn-primary w-full py-2 rounded\">Sign In</button></form><div id=\"result\" class=\"mt-4 text-center\"></div><p class=\"text-center mt-4\"><a href=\"/register\" class=\"text-blue-500 hover:underline\">Don't have an account? Sign Up</a></p></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<form hx-post=\"/api/auth/login\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"card p-6 space-y-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Email or Username</label> <input type=\"text\" name=\"login\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" name=\"password\" class=\"input\" required></div><button type=\"submit\" class=\"btn btn-primary w-full py-2.5\">Sign In</button></form><div id=\"result\" class=\"mt-4 text-center text-sm\"></div><p class=\"text-center mt-6 text-sm\" style=\"color: var(--text-secondary)\">Don't have an account? <a href=\"/register\" class=\"font-medium hover:underline\" style=\"color: var(--accent)\">Sign up</a></p></div></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+23
-12
@@ -11,21 +11,32 @@ templ Profile(user User) {
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }" x-data="profile">
|
||||
@Header(user, "/profile")
|
||||
<main class="max-w-4xl mx-auto px-4 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Profile Settings</h1>
|
||||
<p style="color: var(--text-secondary)">Manage your account information and preferences</p>
|
||||
<main class="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8 flex items-center gap-3">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("user", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Profile Settings</h1>
|
||||
<p class="text-sm mt-0.5" style="color: var(--text-secondary)">Manage your account information and preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-8">
|
||||
<!-- Account Information & Password - uses reusable ProfileForm -->
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<div class="space-y-6">
|
||||
<div class="card p-6">
|
||||
@ProfileForm(user, "/api/auth/profile", true, false, false)
|
||||
</div>
|
||||
<!-- Danger Zone -->
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Danger Zone</h3>
|
||||
<p style="color: var(--text-secondary)" class="mb-4">Once you delete your account, there is no going back. Please be certain.</p>
|
||||
<button @click="confirmDeleteAccount()" class="px-4 py-2 rounded text-sm" style="background-color: #dc2626; color: white;">
|
||||
<div class="card p-6">
|
||||
<div class="flex items-start gap-3 mb-4">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: color-mix(in srgb, var(--status-danger) 18%, transparent); color: var(--status-danger);">
|
||||
@Icon("alert", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Danger Zone</h3>
|
||||
<p class="text-sm mt-0.5" style="color: var(--text-secondary)">Once you delete your account, there is no going back. Please be certain.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="confirmDeleteAccount()" class="btn btn-danger">
|
||||
@Icon("trash", "h-4 w-4")
|
||||
Remove My Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -6,60 +6,57 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
|
||||
hx-target="#profile-result"
|
||||
hx-swap="innerHTML"
|
||||
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
||||
class="space-y-6"
|
||||
class="space-y-8"
|
||||
>
|
||||
<!-- Account Information -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Account Information</h3>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
@Icon("user", "h-5 w-5")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Account Information</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Username</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value={ user.Username }
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email Address</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={ user.Email }
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">First Name</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
value={ user.FirstName }
|
||||
placeholder="Optional"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Last Name</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
value={ user.LastName }
|
||||
placeholder="Optional"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Theme</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Theme</label>
|
||||
<select
|
||||
name="theme"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="tokyo-night" selected?={ user.Theme == "tokyo-night" }>Tokyo Night</option>
|
||||
<option value="dracula" selected?={ user.Theme == "dracula" }>Dracula</option>
|
||||
@@ -71,11 +68,10 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Timezone</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Timezone</label>
|
||||
<select
|
||||
name="timezone"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="UTC" selected?={ user.Timezone == "UTC" }>UTC (UTC+0)</option>
|
||||
<option value="Pacific/Honolulu" selected?={ user.Timezone == "Pacific/Honolulu" }>Hawaii (UTC-10)</option>
|
||||
@@ -105,11 +101,10 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
|
||||
</div>
|
||||
if showRoleField {
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Role</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Role</label>
|
||||
<select
|
||||
name="role"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="user" selected?={ user.Role == "user" }>User</option>
|
||||
<option value="admin" selected?={ user.Role == "admin" }>Admin</option>
|
||||
@@ -118,39 +113,37 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Change Password Section -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Change Password</h3>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
@Icon("shield", "h-5 w-5")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Change Password</h3>
|
||||
</div>
|
||||
if requireCurrentPassword {
|
||||
<!-- Self-edit: require current password -->
|
||||
<div class="space-y-4 max-w-md">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Current Password</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Current Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="current_password"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">New Password</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="new_password"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
minlength="6"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm New Password</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Confirm New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="confirm_password"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
minlength="6"
|
||||
/>
|
||||
</div>
|
||||
@@ -161,36 +154,33 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
|
||||
hx-target="#password-result"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="closest form"
|
||||
class="px-4 py-2 rounded"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
@Icon("save", "h-4 w-4")
|
||||
Update Password
|
||||
</button>
|
||||
<div id="password-result" class="mt-2"></div>
|
||||
</div>
|
||||
} else {
|
||||
<!-- Admin edit: no current password required -->
|
||||
<p class="text-sm italic mb-4" style="color: var(--text-secondary);">
|
||||
As an admin, you can change this user's password without knowing their current password.
|
||||
</p>
|
||||
<div class="space-y-4 max-w-md">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">New Password</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="new_password"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
minlength="6"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm New Password</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Confirm New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="confirm_password"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
minlength="6"
|
||||
/>
|
||||
</div>
|
||||
@@ -201,32 +191,30 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
|
||||
hx-target="#password-result"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="closest form"
|
||||
class="px-4 py-2 rounded"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
@Icon("save", "h-4 w-4")
|
||||
Reset Password
|
||||
</button>
|
||||
<div id="password-result" class="mt-2"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-3 pt-6 border-t" style="border-color: var(--border);">
|
||||
if showCancelButton {
|
||||
<button
|
||||
type="button"
|
||||
@click="closeProfileModal()"
|
||||
class="px-4 py-2 rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 rounded"
|
||||
style="background-color: var(--accent); color: white;"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
@Icon("save", "h-4 w-4")
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+123
-83
@@ -42,33 +42,41 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" hx-target=\"#profile-result\" hx-swap=\"innerHTML\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' class=\"space-y-6\"><!-- Account Information --><div><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary)\">Account Information</h3><div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Username</label> <input type=\"text\" name=\"username\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" hx-target=\"#profile-result\" hx-swap=\"innerHTML\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' class=\"space-y-8\"><div><div class=\"flex items-center gap-2 mb-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("user", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Account Information</h3></div><div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Username</label> <input type=\"text\" name=\"username\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 20, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 22, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Email Address</label> <input type=\"email\" name=\"email\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Email Address</label> <input type=\"email\" name=\"email\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.Email)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 30, Col: 24}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 31, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">First Name</label> <input type=\"text\" name=\"first_name\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">First Name</label> <input type=\"text\" name=\"first_name\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -81,398 +89,430 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" placeholder=\"Optional\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Last Name</label> <input type=\"text\" name=\"last_name\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" placeholder=\"Optional\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Last Name</label> <input type=\"text\" name=\"last_name\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.LastName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 51, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 50, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" placeholder=\"Optional\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Theme</label> <select name=\"theme\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"tokyo-night\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" placeholder=\"Optional\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Theme</label> <select name=\"theme\" class=\"input\"><option value=\"tokyo-night\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Theme == "tokyo-night" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, ">Tokyo Night</option> <option value=\"dracula\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">Tokyo Night</option> <option value=\"dracula\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Theme == "dracula" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ">Dracula</option> <option value=\"nord\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">Dracula</option> <option value=\"nord\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Theme == "nord" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ">Nord</option> <option value=\"solarized-dark\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">Nord</option> <option value=\"solarized-dark\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Theme == "solarized-dark" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ">Solarized Dark</option> <option value=\"monokai\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">Solarized Dark</option> <option value=\"monokai\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Theme == "monokai" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, ">Monokai</option> <option value=\"one-dark-pro\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">Monokai</option> <option value=\"one-dark-pro\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Theme == "one-dark-pro" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, ">One Dark Pro</option> <option value=\"material-dark\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">One Dark Pro</option> <option value=\"material-dark\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Theme == "material-dark" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, ">Material Dark</option></select></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Timezone</label> <select name=\"timezone\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"UTC\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, ">Material Dark</option></select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Timezone</label> <select name=\"timezone\" class=\"input\"><option value=\"UTC\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "UTC" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, ">UTC (UTC+0)</option> <option value=\"Pacific/Honolulu\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, ">UTC (UTC+0)</option> <option value=\"Pacific/Honolulu\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Pacific/Honolulu" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, ">Hawaii (UTC-10)</option> <option value=\"America/Anchorage\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, ">Hawaii (UTC-10)</option> <option value=\"America/Anchorage\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "America/Anchorage" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, ">Alaska (UTC-9/-8)</option> <option value=\"America/Los_Angeles\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, ">Alaska (UTC-9/-8)</option> <option value=\"America/Los_Angeles\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "America/Los_Angeles" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, ">Pacific (UTC-8/-7)</option> <option value=\"America/Denver\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">Pacific (UTC-8/-7)</option> <option value=\"America/Denver\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "America/Denver" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, ">Mountain (UTC-7/-6)</option> <option value=\"America/Phoenix\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, ">Mountain (UTC-7/-6)</option> <option value=\"America/Phoenix\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "America/Phoenix" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, ">Mountain - no DST (UTC-7)</option> <option value=\"America/Chicago\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">Mountain - no DST (UTC-7)</option> <option value=\"America/Chicago\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "America/Chicago" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ">Central (UTC-6/-5)</option> <option value=\"America/New_York\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, ">Central (UTC-6/-5)</option> <option value=\"America/New_York\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "America/New_York" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, ">Eastern (UTC-5/-4)</option> <option value=\"America/Sao_Paulo\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, ">Eastern (UTC-5/-4)</option> <option value=\"America/Sao_Paulo\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "America/Sao_Paulo" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, ">Brasilia (UTC-3/-2)</option> <option value=\"Europe/London\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, ">Brasilia (UTC-3/-2)</option> <option value=\"Europe/London\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Europe/London" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, ">British (UTC+0/+1)</option> <option value=\"Europe/Paris\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">British (UTC+0/+1)</option> <option value=\"Europe/Paris\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Europe/Paris" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, ">Central European (UTC+1/+2)</option> <option value=\"Europe/Helsinki\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">Central European (UTC+1/+2)</option> <option value=\"Europe/Helsinki\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Europe/Helsinki" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, ">Eastern European (UTC+2/+3)</option> <option value=\"Europe/Moscow\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, ">Eastern European (UTC+2/+3)</option> <option value=\"Europe/Moscow\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Europe/Moscow" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, ">Moscow (UTC+3)</option> <option value=\"Asia/Tehran\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">Moscow (UTC+3)</option> <option value=\"Asia/Tehran\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Asia/Tehran" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, ">Iran (UTC+3:30)</option> <option value=\"Asia/Dubai\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, ">Iran (UTC+3:30)</option> <option value=\"Asia/Dubai\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Asia/Dubai" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, ">Gulf (UTC+4)</option> <option value=\"Asia/Karachi\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">Gulf (UTC+4)</option> <option value=\"Asia/Karachi\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Asia/Karachi" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, ">Pakistan (UTC+5)</option> <option value=\"Asia/Kolkata\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, ">Pakistan (UTC+5)</option> <option value=\"Asia/Kolkata\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Asia/Kolkata" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, ">India (UTC+5:30)</option> <option value=\"Asia/Dhaka\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, ">India (UTC+5:30)</option> <option value=\"Asia/Dhaka\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Asia/Dhaka" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, ">Bangladesh (UTC+6)</option> <option value=\"Asia/Bangkok\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, ">Bangladesh (UTC+6)</option> <option value=\"Asia/Bangkok\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Asia/Bangkok" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, ">Indochina (UTC+7)</option> <option value=\"Asia/Shanghai\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, ">Indochina (UTC+7)</option> <option value=\"Asia/Shanghai\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Asia/Shanghai" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, ">China (UTC+8)</option> <option value=\"Asia/Tokyo\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, ">China (UTC+8)</option> <option value=\"Asia/Tokyo\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Asia/Tokyo" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, ">Japan/Korea (UTC+9)</option> <option value=\"Australia/Darwin\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, ">Japan/Korea (UTC+9)</option> <option value=\"Australia/Darwin\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Australia/Darwin" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, ">Australian Central (UTC+9:30)</option> <option value=\"Australia/Sydney\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, ">Australian Central (UTC+9:30)</option> <option value=\"Australia/Sydney\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Australia/Sydney" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, ">Australian Eastern (UTC+10/+11)</option> <option value=\"Pacific/Auckland\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, ">Australian Eastern (UTC+10/+11)</option> <option value=\"Pacific/Auckland\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Timezone == "Pacific/Auckland" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, ">New Zealand (UTC+12/+13)</option></select></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, ">New Zealand (UTC+12/+13)</option></select></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if showRoleField {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Role</label> <select name=\"role\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"user\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Role</label> <select name=\"role\" class=\"input\"><option value=\"user\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "user" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, ">User</option> <option value=\"admin\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, ">User</option> <option value=\"admin\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "admin" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, ">Admin</option></select></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, ">Admin</option></select></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</div></div><!-- Change Password Section --><div><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary)\">Change Password</h3>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</div></div><div><div class=\"flex items-center gap-2 mb-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("shield", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Change Password</h3></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if requireCurrentPassword {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<!-- Self-edit: require current password --> <div class=\"space-y-4 max-w-md\"><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Current Password</label> <input type=\"password\" name=\"current_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" required></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">New Password</label> <input type=\"password\" name=\"new_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" minlength=\"6\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Confirm New Password</label> <input type=\"password\" name=\"confirm_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" minlength=\"6\"></div><button type=\"button\" hx-put=\"/api/auth/password\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"#password-result\" hx-swap=\"innerHTML\" hx-include=\"closest form\" class=\"px-4 py-2 rounded\" style=\"background-color: var(--accent); color: white;\">Update Password</button><div id=\"password-result\" class=\"mt-2\"></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<div class=\"space-y-4 max-w-md\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Current Password</label> <input type=\"password\" name=\"current_password\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">New Password</label> <input type=\"password\" name=\"new_password\" class=\"input\" minlength=\"6\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Confirm New Password</label> <input type=\"password\" name=\"confirm_password\" class=\"input\" minlength=\"6\"></div><button type=\"button\" hx-put=\"/api/auth/password\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"#password-result\" hx-swap=\"innerHTML\" hx-include=\"closest form\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "Update Password</button><div id=\"password-result\" class=\"mt-2\"></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<!-- Admin edit: no current password required --> <p class=\"text-sm italic mb-4\" style=\"color: var(--text-secondary);\">As an admin, you can change this user's password without knowing their current password.</p><div class=\"space-y-4 max-w-md\"><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">New Password</label> <input type=\"password\" name=\"new_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" minlength=\"6\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Confirm New Password</label> <input type=\"password\" name=\"confirm_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" minlength=\"6\"></div><button type=\"button\" hx-put=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<p class=\"text-sm italic mb-4\" style=\"color: var(--text-secondary);\">As an admin, you can change this user's password without knowing their current password.</p><div class=\"space-y-4 max-w-md\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">New Password</label> <input type=\"password\" name=\"new_password\" class=\"input\" minlength=\"6\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Confirm New Password</label> <input type=\"password\" name=\"confirm_password\" class=\"input\" minlength=\"6\"></div><button type=\"button\" hx-put=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/password/" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 199, Col: 46}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 189, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"#password-result\" hx-swap=\"innerHTML\" hx-include=\"closest form\" class=\"px-4 py-2 rounded\" style=\"background-color: var(--accent); color: white;\">Reset Password</button><div id=\"password-result\" class=\"mt-2\"></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"#password-result\" hx-swap=\"innerHTML\" hx-include=\"closest form\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "Reset Password</button><div id=\"password-result\" class=\"mt-2\"></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</div><!-- Actions --><div class=\"flex justify-end gap-3 pt-6 border-t\" style=\"border-color: var(--border);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</div><div class=\"flex justify-end gap-3 pt-6 border-t\" style=\"border-color: var(--border);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if showCancelButton {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<button type=\"button\" @click=\"closeProfileModal()\" class=\"px-4 py-2 rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);\">Cancel</button> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<button type=\"button\" @click=\"closeProfileModal()\" class=\"btn btn-secondary\">Cancel</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<button type=\"submit\" class=\"px-4 py-2 rounded\" style=\"background-color: var(--accent); color: white;\">Save Changes</button></div><div id=\"profile-result\" class=\"mt-2\"></div></form>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "<button type=\"submit\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "Save Changes</button></div><div id=\"profile-result\" class=\"mt-2\"></div></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
package templates
|
||||
|
||||
templ ProfileModal(user User) {
|
||||
<div id="profile-modal" class="fixed inset-0 flex items-center justify-center z-50" x-data="profileModal" x-init="setupProfileModal()" style="background-color: rgba(0,0,0,0.5);">
|
||||
<div class="rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto" style="background-color: var(--bg-secondary);">
|
||||
<!-- Modal Header -->
|
||||
<div class="flex justify-between items-center p-6 border-b" style="border-color: var(--border);">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Edit User Profile</h2>
|
||||
<p class="text-sm mt-1" style="color: var(--text-secondary);">
|
||||
{ user.Username } ({ user.Email })
|
||||
</p>
|
||||
<div id="profile-modal" class="fixed inset-0 flex items-center justify-center z-50 p-4" x-data="profileModal" x-init="setupProfileModal()" style="background-color: var(--surface-overlay);">
|
||||
<div class="card w-full max-w-4xl max-h-[90vh] overflow-y-auto" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-start gap-4 p-6 border-b" style="border-color: var(--border);">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("edit", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit User Profile</h2>
|
||||
<p class="text-sm mt-0.5" style="color: var(--text-secondary);">
|
||||
{ user.Username } ({ user.Email })
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="closeProfileModal()"
|
||||
class="text-2xl"
|
||||
style="color: var(--text-secondary);"
|
||||
class="icon-btn shrink-0"
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body - uses reusable form -->
|
||||
<div class="p-6">
|
||||
@ProfileForm(user, "/api/auth/profile/" + user.ID, false, true, true)
|
||||
</div>
|
||||
|
||||
@@ -29,33 +29,49 @@ func ProfileModal(user User) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"profile-modal\" class=\"fixed inset-0 flex items-center justify-center z-50\" x-data=\"profileModal\" x-init=\"setupProfileModal()\" style=\"background-color: rgba(0,0,0,0.5);\"><div class=\"rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto\" style=\"background-color: var(--bg-secondary);\"><!-- Modal Header --><div class=\"flex justify-between items-center p-6 border-b\" style=\"border-color: var(--border);\"><div><h2 class=\"text-2xl font-bold\" style=\"color: var(--text-primary)\">Edit User Profile</h2><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"profile-modal\" class=\"fixed inset-0 flex items-center justify-center z-50 p-4\" x-data=\"profileModal\" x-init=\"setupProfileModal()\" style=\"background-color: var(--surface-overlay);\"><div class=\"card w-full max-w-4xl max-h-[90vh] overflow-y-auto\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-start gap-4 p-6 border-b\" style=\"border-color: var(--border);\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("edit", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span><div><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Edit User Profile</h2><p class=\"text-sm mt-0.5\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 11, Col: 21}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 14, Col: 22}
|
||||
}
|
||||
_, 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, " (")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " (")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 11, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 14, 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, 3, ")</p></div><button @click=\"closeProfileModal()\" class=\"text-2xl\" style=\"color: var(--text-secondary);\">×</button></div><!-- Modal Body - uses reusable form --><div class=\"p-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, ")</p></div></div><button @click=\"closeProfileModal()\" class=\"icon-btn shrink-0\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</button></div><div class=\"p-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -63,7 +79,7 @@ func ProfileModal(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -37,7 +37,15 @@ func Profile(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"max-w-4xl mx-auto px-4 py-8\"><div class=\"mb-8\"><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">Profile Settings</h1><p style=\"color: var(--text-secondary)\">Manage your account information and preferences</p></div><div class=\"space-y-8\"><!-- Account Information & Password - uses reusable ProfileForm --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8 flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("user", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><div><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Profile Settings</h1><p class=\"text-sm mt-0.5\" style=\"color: var(--text-secondary)\">Manage your account information and preferences</p></div></div><div class=\"space-y-6\"><div class=\"card p-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -45,7 +53,23 @@ func Profile(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><!-- Danger Zone --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h3 class=\"text-xl font-semibold mb-4\" style=\"color: var(--text-primary)\">Danger Zone</h3><p style=\"color: var(--text-secondary)\" class=\"mb-4\">Once you delete your account, there is no going back. Please be certain.</p><button @click=\"confirmDeleteAccount()\" class=\"px-4 py-2 rounded text-sm\" style=\"background-color: #dc2626; color: white;\">Remove My Account</button></div></div></main></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div class=\"card p-6\"><div class=\"flex items-start gap-3 mb-4\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: color-mix(in srgb, var(--status-danger) 18%, transparent); color: var(--status-danger);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("alert", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span><div><h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Danger Zone</h3><p class=\"text-sm mt-0.5\" style=\"color: var(--text-secondary)\">Once you delete your account, there is no going back. Please be certain.</p></div></div><button @click=\"confirmDeleteAccount()\" class=\"btn btn-danger\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Remove My Account</button></div></div></main></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+48
-44
@@ -16,90 +16,94 @@ templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessag
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, "/progress")
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Reading Progress</h1>
|
||||
<p style="color: var(--text-secondary)">Track your reading progress across all devices</p>
|
||||
<div class="mb-8 flex items-center gap-3">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("book-open", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Reading Progress</h1>
|
||||
<p class="text-sm mt-0.5" style="color: var(--text-secondary)">Track your reading progress across all devices</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="progress-container" class="space-y-6">
|
||||
if len(progressData) == 0 {
|
||||
<div class="text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">📖</div>
|
||||
<div class="card text-center py-16 px-6" style="color: var(--text-secondary)">
|
||||
<div class="flex justify-center mb-4">
|
||||
@Icon("book-open", "h-12 w-12 opacity-50")
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Progress Yet</h3>
|
||||
<p>Start reading a book to track your progress</p>
|
||||
</div>
|
||||
}
|
||||
for _, item := range progressData {
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="card p-6">
|
||||
<div class="flex gap-6">
|
||||
<div class="flex-shrink-0">
|
||||
<img
|
||||
src="{ item.CoverImagePath }"
|
||||
src={ item.CoverImagePath }
|
||||
alt="Cover"
|
||||
class="w-24 h-32 object-cover rounded"
|
||||
class="w-24 h-32 object-cover rounded-lg"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex justify-between items-start gap-4 mb-3">
|
||||
<div class="min-w-0">
|
||||
<a
|
||||
href={ "/media/" + item.MediaItemID.String() }
|
||||
class="inline-block hover:underline"
|
||||
style="color: var(--text-primary); text-decoration: none;"
|
||||
>
|
||||
<h3 class="font-semibold text-lg hover:opacity-80">{ item.Title }</h3>
|
||||
<h3 class="font-semibold text-lg">{ item.Title }</h3>
|
||||
</a>
|
||||
if item.Author != "" {
|
||||
<p class="text-sm" style="color: var(--text-secondary)">by { item.Author }</p>
|
||||
<p class="text-sm mt-0.5" style="color: var(--text-secondary)">by { item.Author }</p>
|
||||
}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-2xl font-bold" style="color: var(--accent)">{ item.ProgressPercentage }%</span>
|
||||
</div>
|
||||
<span class="text-2xl font-bold shrink-0" style="color: var(--accent)">{ fmt.Sprintf("%.2f", item.ProgressPercentage) }%</span>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="w-full bg-gray-700 rounded-full h-3">
|
||||
<div class="h-3 rounded-full transition-all" style={ "width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%; background-color: var(--accent);" }></div>
|
||||
<div class="w-full rounded-full h-2.5" style="background-color: color-mix(in srgb, var(--text-primary) 12%, transparent);">
|
||||
<div class="h-2.5 rounded-full transition-all" style={ "width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%; background-color: var(--accent);" }></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Current Position</p>
|
||||
<p style="color: var(--text-primary)">
|
||||
if item.FormatGroup == "reflowable" {
|
||||
if item.EstimatedPages > 0 {
|
||||
{ fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages) }
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div class="stat-card">
|
||||
<span class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Current Position</span>
|
||||
<p class="text-sm font-medium" style="color: var(--text-primary)">
|
||||
if item.FormatGroup == "reflowable" {
|
||||
if item.EstimatedPages > 0 {
|
||||
{ fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages) }
|
||||
} else {
|
||||
{ fmt.Sprintf("%.2f%%", item.ProgressPercentage) }
|
||||
}
|
||||
} else {
|
||||
{ fmt.Sprintf("%.1f%%", item.ProgressPercentage) }
|
||||
{ fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages) }
|
||||
}
|
||||
} else {
|
||||
{ fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages) }
|
||||
}
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Last Updated</p>
|
||||
<p style="color: var(--text-primary)">{ item.LastUpdated }</p>
|
||||
<div class="stat-card">
|
||||
<span class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Last Updated</span>
|
||||
<p class="text-sm font-medium" style="color: var(--text-primary)">{ item.LastUpdated }</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style="color: var(--text-secondary)">Synced From</p>
|
||||
<div class="stat-card">
|
||||
<span class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Synced From</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xl">{ item.DeviceIcon }</span>
|
||||
<span style="color: var(--text-primary)">{ item.DeviceName }</span>
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">{ item.DeviceName }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
if item.DeviceIcon != "" {
|
||||
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
|
||||
<div class="flex items-center gap-2 text-xs" style="color: var(--text-secondary);">
|
||||
<span>🔄</span>
|
||||
<span>Last sync from <strong>{ item.DeviceName }</strong></span>
|
||||
<span>({ item.DeviceType })</span>
|
||||
</div>
|
||||
<div class="mt-3 pt-3 border-t flex items-center gap-2 text-xs" style="border-color: var(--border); color: var(--text-secondary);">
|
||||
@Icon("sync", "h-4 w-4")
|
||||
<span>Last sync from <strong style="color: var(--text-primary)">{ item.DeviceName }</strong></span>
|
||||
<span>({ item.DeviceType })</span>
|
||||
</div>
|
||||
}
|
||||
if item.EpubCFI != "" {
|
||||
<div class="mt-2 text-xs" style="color: var(--text-secondary);">
|
||||
<span>📍</span>
|
||||
<div class="mt-2 flex items-center gap-2 text-xs" style="color: var(--text-secondary);">
|
||||
@Icon("bookmark", "h-3.5 w-3.5")
|
||||
<span>CFI: { item.EpubCFI }</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
+139
-94
@@ -40,229 +40,274 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">Reading Progress</h1><p style=\"color: var(--text-secondary)\">Track your reading progress across all devices</p></div><div id=\"progress-container\" class=\"space-y-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8 flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><div><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Reading Progress</h1><p class=\"text-sm mt-0.5\" style=\"color: var(--text-secondary)\">Track your reading progress across all devices</p></div></div><div id=\"progress-container\" class=\"space-y-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(progressData) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📖</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Progress Yet</h3><p>Start reading a book to track your progress</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"card text-center py-16 px-6\" style=\"color: var(--text-secondary)\"><div class=\"flex justify-center mb-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("book-open", "h-12 w-12 opacity-50").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Progress Yet</h3><p>Start reading a book to track your progress</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, item := range progressData {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex gap-6\"><div class=\"flex-shrink-0\"><img src=\"{ item.CoverImagePath }\" alt=\"Cover\" class=\"w-24 h-32 object-cover rounded\" onerror=\"this.src='/static/placeholder-book.svg'\"></div><div class=\"flex-1\"><div class=\"flex justify-between items-start mb-2\"><div><a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"card p-6\"><div class=\"flex gap-6\"><div class=\"flex-shrink-0\"><img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID.String())
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 46, Col: 56}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 43, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" style=\"color: var(--text-primary); text-decoration: none;\"><h3 class=\"font-semibold text-lg hover:opacity-80\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" alt=\"Cover\" class=\"w-24 h-32 object-cover rounded-lg\" onerror=\"this.src='/static/placeholder-book.svg'\"></div><div class=\"flex-1 min-w-0\"><div class=\"flex justify-between items-start gap-4 mb-3\"><div class=\"min-w-0\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
var templ_7745c5c3_Var3 templ.SafeURL
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID.String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 49, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 53, Col: 56}
|
||||
}
|
||||
_, 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, 6, "</h3></a> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"inline-block hover:underline\" style=\"color: var(--text-primary); text-decoration: none;\"><h3 class=\"font-semibold text-lg\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 57, Col: 58}
|
||||
}
|
||||
_, 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, 9, "</h3></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.Author != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">by ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<p class=\"text-sm mt-0.5\" style=\"color: var(--text-secondary)\">by ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 52, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 60, Col: 91}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
_, 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, 8, "</p>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><div class=\"text-right\"><span class=\"text-2xl font-bold\" style=\"color: var(--accent)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.ProgressPercentage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 56, Col: 98}
|
||||
}
|
||||
_, 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, 10, "%</span></div></div><div class=\"mb-4\"><div class=\"w-full bg-gray-700 rounded-full h-3\"><div class=\"h-3 rounded-full transition-all\" style=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><span class=\"text-2xl font-bold shrink-0\" style=\"color: var(--accent)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%; background-color: var(--accent);")
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", item.ProgressPercentage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 61, Col: 160}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 63, Col: 127}
|
||||
}
|
||||
_, 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, 11, "\"></div></div></div><div class=\"grid grid-cols-1 md:grid-cols-3 gap-4 text-sm\"><div><p style=\"color: var(--text-secondary)\">Current Position</p><p style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "%</span></div><div class=\"mb-4\"><div class=\"w-full rounded-full h-2.5\" style=\"background-color: color-mix(in srgb, var(--text-primary) 12%, transparent);\"><div class=\"h-2.5 rounded-full transition-all\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%; background-color: var(--accent);")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 67, Col: 162}
|
||||
}
|
||||
_, 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, 14, "\"></div></div></div><div class=\"grid grid-cols-1 md:grid-cols-3 gap-3\"><div class=\"stat-card\"><span class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Current Position</span><p class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.FormatGroup == "reflowable" {
|
||||
if item.EstimatedPages > 0 {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 70, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f%%", item.ProgressPercentage))
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 72, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 76, Col: 90}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f%%", item.ProgressPercentage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 78, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages))
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 75, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 81, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
_, 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, 12, "</p></div><div><p style=\"color: var(--text-secondary)\">Last Updated</p><p style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.LastUpdated)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 81, Col: 67}
|
||||
}
|
||||
_, 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, 13, "</p></div><div><p style=\"color: var(--text-secondary)\">Synced From</p><div class=\"flex items-center gap-2\"><span class=\"text-xl\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p></div><div class=\"stat-card\"><span class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Last Updated</span><p class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceIcon)
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.LastUpdated)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 86, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 87, Col: 95}
|
||||
}
|
||||
_, 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, 14, "</span> <span style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p></div><div class=\"stat-card\"><span class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Synced From</span><div class=\"flex items-center gap-2\"><span class=\"text-xl\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceIcon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 87, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 92, Col: 51}
|
||||
}
|
||||
_, 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, 15, "</span></div></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span> <span class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 93, Col: 98}
|
||||
}
|
||||
_, 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, "</span></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.DeviceIcon != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"mt-3 pt-3 border-t\" style=\"border-color: var(--border);\"><div class=\"flex items-center gap-2 text-xs\" style=\"color: var(--text-secondary);\"><span>🔄</span> <span>Last sync from <strong>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"mt-3 pt-3 border-t flex items-center gap-2 text-xs\" style=\"border-color: var(--border); color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 95, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
templ_7745c5c3_Err = Icon("sync", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</strong></span> <span>(")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span>Last sync from <strong style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceType)
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 96, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 100, Col: 92}
|
||||
}
|
||||
_, 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, 18, ")</span></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if item.EpubCFI != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"mt-2 text-xs\" style=\"color: var(--text-secondary);\"><span>📍</span> <span>CFI: ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</strong></span> <span>(")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(item.EpubCFI)
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 103, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 101, Col: 35}
|
||||
}
|
||||
_, 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, "</span></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, ")</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div></div></div>")
|
||||
if item.EpubCFI != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"mt-2 flex items-center gap-2 text-xs\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("bookmark", "h-3.5 w-3.5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<span>CFI: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(item.EpubCFI)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 107, Col: 36}
|
||||
}
|
||||
_, 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, 25, "</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -270,7 +315,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+63
-55
@@ -14,53 +14,54 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
|
||||
</head>
|
||||
<body x-data="queue" class="theme-{ user.Theme }">
|
||||
@Header(user, "/queue")
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex justify-between items-center gap-4 flex-wrap">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Sync Queue</h1>
|
||||
<p style="color: var(--text-secondary)">Monitor and manage offline sync queue</p>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("clock", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Sync Queue</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Monitor and manage offline sync queue</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button @click="processPendingItems()" class="btn-primary px-4 py-2 rounded-lg">
|
||||
<button @click="processPendingItems()" class="btn btn-primary">
|
||||
@Icon("play", "h-4 w-4")
|
||||
Process Queue
|
||||
</button>
|
||||
<button @click="clearFailedItems()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Clear Failed
|
||||
</button>
|
||||
<button @click="clearAllItems()" class="btn-danger px-4 py-2 rounded-lg">
|
||||
Clear All
|
||||
</button>
|
||||
<button @click="clearFailedItems()" class="btn btn-secondary">Clear Failed</button>
|
||||
<button @click="clearAllItems()" class="btn btn-danger">Clear All</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stats-bar" class="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="text-3xl font-bold" style="color: #f59e0b;">{ stats.PendingCount }</div>
|
||||
<div class="text-sm" style="color: var(--text-secondary);">Pending</div>
|
||||
<div class="stat-card text-center">
|
||||
<div class="text-3xl font-bold" style="color: var(--status-warning);">{ stats.PendingCount }</div>
|
||||
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Pending</div>
|
||||
</div>
|
||||
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="text-3xl font-bold" style="color: #3b82f6;">{ stats.ProcessingCount }</div>
|
||||
<div class="text-sm" style="color: var(--text-secondary);">Processing</div>
|
||||
<div class="stat-card text-center">
|
||||
<div class="text-3xl font-bold" style="color: var(--status-info);">{ stats.ProcessingCount }</div>
|
||||
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Processing</div>
|
||||
</div>
|
||||
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="text-3xl font-bold" style="color: #10b981;">{ stats.CompletedCount }</div>
|
||||
<div class="text-sm" style="color: var(--text-secondary);">Completed</div>
|
||||
<div class="stat-card text-center">
|
||||
<div class="text-3xl font-bold" style="color: var(--status-success);">{ stats.CompletedCount }</div>
|
||||
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Completed</div>
|
||||
</div>
|
||||
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="text-3xl font-bold" style="color: #ef4444;">{ stats.FailedCount }</div>
|
||||
<div class="text-sm" style="color: var(--text-secondary);">Failed</div>
|
||||
<div class="stat-card text-center">
|
||||
<div class="text-3xl font-bold" style="color: var(--status-danger);">{ stats.FailedCount }</div>
|
||||
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card p-4 rounded-lg border mb-6" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="flex flex-wrap gap-4 items-center">
|
||||
<div class="card p-4 mb-6">
|
||||
<div class="flex flex-wrap gap-4 items-end">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Status Filter</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Status Filter</label>
|
||||
<select
|
||||
id="status-filter"
|
||||
onchange="filterQueue()"
|
||||
class="px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="all">All Items</option>
|
||||
<option value="pending" selected>Pending</option>
|
||||
@@ -70,12 +71,11 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Type</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Sync Type</label>
|
||||
<select
|
||||
id="type-filter"
|
||||
onchange="filterQueue()"
|
||||
class="px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="all">All Types</option>
|
||||
<option value="progress">Progress</option>
|
||||
@@ -85,18 +85,18 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device</label>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device</label>
|
||||
<select
|
||||
id="device-filter"
|
||||
onchange="filterQueue()"
|
||||
class="px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
>
|
||||
<option value="all">All Devices</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
<button @click="refreshQueue()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
<button @click="refreshQueue()" class="btn btn-secondary">
|
||||
@Icon("refresh", "h-4 w-4")
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
@@ -105,27 +105,31 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
|
||||
<div class="loading-spinner mx-auto mb-4"></div>
|
||||
<p>Loading queue...</p>
|
||||
</div>
|
||||
<div id="empty-state" class="hidden text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">📭</div>
|
||||
<div id="empty-state" class="hidden card text-center py-16">
|
||||
<span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("check-circle", "h-7 w-7")
|
||||
</span>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">Queue Empty</h3>
|
||||
<p>No items in the sync queue</p>
|
||||
<p style="color: var(--text-secondary)">No items in the sync queue</p>
|
||||
</div>
|
||||
<div id="queue-list" class="space-y-3">
|
||||
if len(queueItems) == 0 {
|
||||
<div class="text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">📭</div>
|
||||
<div class="card text-center py-16">
|
||||
<span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("check-circle", "h-7 w-7")
|
||||
</span>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">Queue Empty</h3>
|
||||
<p>No items in the sync queue</p>
|
||||
<p style="color: var(--text-secondary)">No items in the sync queue</p>
|
||||
</div>
|
||||
}
|
||||
for _, item := range queueItems {
|
||||
<div class="card p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="inline-block px-2 py-0.5 rounded text-[11px] font-semibold uppercase status-{ item.Status }">{ item.Status }</span>
|
||||
<div class="card p-4 transition-colors" style="cursor: default;">
|
||||
<div class="flex justify-between items-start mb-2 gap-2">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="badge status-{ item.Status }">{ item.Status }</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">{ item.SyncType }</span>
|
||||
</div>
|
||||
<span class="inline-block px-2 py-0.5 rounded text-[10px] font-semibold priority-{ item.Priority }">P{ item.Priority }</span>
|
||||
<span class="badge priority-{ item.Priority }">P{ item.Priority }</span>
|
||||
</div>
|
||||
<div class="text-sm mb-2" style="color: var(--text-secondary)">
|
||||
Device ID: { item.DeviceID }
|
||||
@@ -138,25 +142,29 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
|
||||
<span>{ item.CreatedAt }</span>
|
||||
</div>
|
||||
if item.ErrorMessage != nil {
|
||||
<div class="mt-2 text-sm" style="color: #ef4444;">
|
||||
Error: { *item.ErrorMessage }
|
||||
<div class="mt-2 text-sm flex items-start gap-2" style="color: var(--status-danger);">
|
||||
@Icon("alert", "h-4 w-4 shrink-0 mt-0.5")
|
||||
<span>Error: { *item.ErrorMessage }</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div id="queue-item-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-2xl mx-4 my-8" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div id="queue-item-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-2xl my-8" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Queue Item Details</h2>
|
||||
<button @click="hideQueueItemModal()" class="p-2 hover:opacity-80 rounded-lg" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
X
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Queue Item Details</h2>
|
||||
<button @click="hideQueueItemModal()" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<div id="queue-item-details"></div>
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button @click="hideQueueItemModal()" class="btn-secondary px-4 py-2 rounded-lg">Close</button>
|
||||
<button @click="retryQueueItem()" class="btn-primary px-4 py-2 rounded-lg">Retry</button>
|
||||
<button @click="hideQueueItemModal()" class="btn btn-secondary">Close</button>
|
||||
<button @click="retryQueueItem()" class="btn btn-primary">
|
||||
@Icon("refresh", "h-4 w-4")
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+97
-33
@@ -39,208 +39,272 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><div class=\"flex justify-between items-center\"><div><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">Sync Queue</h1><p style=\"color: var(--text-secondary)\">Monitor and manage offline sync queue</p></div><div class=\"flex space-x-3\"><button @click=\"processPendingItems()\" class=\"btn-primary px-4 py-2 rounded-lg\">Process Queue</button> <button @click=\"clearFailedItems()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Clear Failed</button> <button @click=\"clearAllItems()\" class=\"btn-danger px-4 py-2 rounded-lg\">Clear All</button></div></div><div id=\"stats-bar\" class=\"mt-4 grid grid-cols-2 md:grid-cols-4 gap-4\"><div class=\"card p-4 rounded-lg border text-center\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"text-3xl font-bold\" style=\"color: #f59e0b;\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><div class=\"flex justify-between items-center gap-4 flex-wrap\"><div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("clock", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Sync Queue</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Monitor and manage offline sync queue</p></div><div class=\"flex space-x-3\"><button @click=\"processPendingItems()\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("play", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Process Queue</button> <button @click=\"clearFailedItems()\" class=\"btn btn-secondary\">Clear Failed</button> <button @click=\"clearAllItems()\" class=\"btn btn-danger\">Clear All</button></div></div><div id=\"stats-bar\" class=\"mt-4 grid grid-cols-2 md:grid-cols-4 gap-4\"><div class=\"stat-card text-center\"><div class=\"text-3xl font-bold\" style=\"color: var(--status-warning);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.PendingCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 38, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 40, Col: 97}
|
||||
}
|
||||
_, 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, 3, "</div><div class=\"text-sm\" style=\"color: var(--text-secondary);\">Pending</div></div><div class=\"card p-4 rounded-lg border text-center\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"text-3xl font-bold\" style=\"color: #3b82f6;\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Pending</div></div><div class=\"stat-card text-center\"><div class=\"text-3xl font-bold\" style=\"color: var(--status-info);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.ProcessingCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 42, Col: 86}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 44, Col: 97}
|
||||
}
|
||||
_, 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, 4, "</div><div class=\"text-sm\" style=\"color: var(--text-secondary);\">Processing</div></div><div class=\"card p-4 rounded-lg border text-center\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"text-3xl font-bold\" style=\"color: #10b981;\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Processing</div></div><div class=\"stat-card text-center\"><div class=\"text-3xl font-bold\" style=\"color: var(--status-success);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.CompletedCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 46, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 48, Col: 99}
|
||||
}
|
||||
_, 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, 5, "</div><div class=\"text-sm\" style=\"color: var(--text-secondary);\">Completed</div></div><div class=\"card p-4 rounded-lg border text-center\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"text-3xl font-bold\" style=\"color: #ef4444;\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><div class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Completed</div></div><div class=\"stat-card text-center\"><div class=\"text-3xl font-bold\" style=\"color: var(--status-danger);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(stats.FailedCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 50, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 52, Col: 95}
|
||||
}
|
||||
_, 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, "</div><div class=\"text-sm\" style=\"color: var(--text-secondary);\">Failed</div></div></div></div><div class=\"card p-4 rounded-lg border mb-6\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex flex-wrap gap-4 items-center\"><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Status Filter</label> <select id=\"status-filter\" onchange=\"filterQueue()\" class=\"px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"all\">All Items</option> <option value=\"pending\" selected>Pending</option> <option value=\"processing\">Processing</option> <option value=\"completed\">Completed</option> <option value=\"failed\">Failed</option></select></div><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Sync Type</label> <select id=\"type-filter\" onchange=\"filterQueue()\" class=\"px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"all\">All Types</option> <option value=\"progress\">Progress</option> <option value=\"note\">Notes</option> <option value=\"highlight\">Highlights</option> <option value=\"bookmark\">Bookmarks</option></select></div><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Device</label> <select id=\"device-filter\" onchange=\"filterQueue()\" class=\"px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"all\">All Devices</option></select></div><div class=\"flex-1\"></div><button @click=\"refreshQueue()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Refresh</button></div></div><div id=\"loading\" class=\"hidden text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading queue...</p></div><div id=\"empty-state\" class=\"hidden text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📭</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">Queue Empty</h3><p>No items in the sync queue</p></div><div id=\"queue-list\" class=\"space-y-3\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><div class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Failed</div></div></div></div><div class=\"card p-4 mb-6\"><div class=\"flex flex-wrap gap-4 items-end\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Status Filter</label> <select id=\"status-filter\" onchange=\"filterQueue()\" class=\"input\"><option value=\"all\">All Items</option> <option value=\"pending\" selected>Pending</option> <option value=\"processing\">Processing</option> <option value=\"completed\">Completed</option> <option value=\"failed\">Failed</option></select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Sync Type</label> <select id=\"type-filter\" onchange=\"filterQueue()\" class=\"input\"><option value=\"all\">All Types</option> <option value=\"progress\">Progress</option> <option value=\"note\">Notes</option> <option value=\"highlight\">Highlights</option> <option value=\"bookmark\">Bookmarks</option></select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Device</label> <select id=\"device-filter\" onchange=\"filterQueue()\" class=\"input\"><option value=\"all\">All Devices</option></select></div><div class=\"flex-1\"></div><button @click=\"refreshQueue()\" class=\"btn btn-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("refresh", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Refresh</button></div></div><div id=\"loading\" class=\"hidden text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading queue...</p></div><div id=\"empty-state\" class=\"hidden card text-center py-16\"><span class=\"grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("check-circle", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">Queue Empty</h3><p style=\"color: var(--text-secondary)\">No items in the sync queue</p></div><div id=\"queue-list\" class=\"space-y-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(queueItems) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📭</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">Queue Empty</h3><p>No items in the sync queue</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"card text-center py-16\"><span class=\"grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("check-circle", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">Queue Empty</h3><p style=\"color: var(--text-secondary)\">No items in the sync queue</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, item := range queueItems {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"card p-4 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-start mb-2\"><div class=\"flex items-center space-x-3\"><span class=\"inline-block px-2 py-0.5 rounded text-[11px] font-semibold uppercase status-{ item.Status }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"card p-4 transition-colors\" style=\"cursor: default;\"><div class=\"flex justify-between items-start mb-2 gap-2\"><div class=\"flex items-center gap-2 flex-wrap\"><span class=\"badge status-{ item.Status }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.Status)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 125, Col: 128}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 129, Col: 65}
|
||||
}
|
||||
_, 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, 9, "</span> <span class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span> <span class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.SyncType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 126, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 130, Col: 83}
|
||||
}
|
||||
_, 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, 10, "</span></div><span class=\"inline-block px-2 py-0.5 rounded text-[10px] font-semibold priority-{ item.Priority }\">P")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></div><span class=\"badge priority-{ item.Priority }\">P")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Priority)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 128, Col: 124}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 132, Col: 71}
|
||||
}
|
||||
_, 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, 11, "</span></div><div class=\"text-sm mb-2\" style=\"color: var(--text-secondary)\">Device ID: ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></div><div class=\"text-sm mb-2\" style=\"color: var(--text-secondary)\">Device ID: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 131, Col: 34}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 135, Col: 34}
|
||||
}
|
||||
_, 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, " ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.MediaTitle != nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "| Book: ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "| Book: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(*item.MediaTitle)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 133, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 137, Col: 35}
|
||||
}
|
||||
_, 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, 14, "</div><div class=\"flex justify-between items-center text-sm\" style=\"color: var(--text-secondary)\"><span>Attempts: ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div><div class=\"flex justify-between items-center text-sm\" style=\"color: var(--text-secondary)\"><span>Attempts: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Attempts)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 137, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 141, Col: 39}
|
||||
}
|
||||
_, 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, "/")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "/")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.MaxAttempts)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 137, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 141, Col: 60}
|
||||
}
|
||||
_, 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, 16, "</span> <span>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.CreatedAt)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 138, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 142, Col: 30}
|
||||
}
|
||||
_, 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, 17, "</span></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.ErrorMessage != nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"mt-2 text-sm\" style=\"color: #ef4444;\">Error: ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"mt-2 text-sm flex items-start gap-2\" style=\"color: var(--status-danger);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("alert", "h-4 w-4 shrink-0 mt-0.5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<span>Error: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(*item.ErrorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 142, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 147, Col: 42}
|
||||
}
|
||||
_, 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, 19, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div><div id=\"queue-item-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-2xl mx-4 my-8\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-2xl font-bold\" style=\"color: var(--text-primary)\">Queue Item Details</h2><button @click=\"hideQueueItemModal()\" class=\"p-2 hover:opacity-80 rounded-lg\" style=\"color: var(--text-primary); background-color: var(--bg-primary);\">X</button></div><div id=\"queue-item-details\"></div><div class=\"flex justify-end space-x-3 mt-6\"><button @click=\"hideQueueItemModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Close</button> <button @click=\"retryQueueItem()\" class=\"btn-primary px-4 py-2 rounded-lg\">Retry</button></div></div></div></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div><div id=\"queue-item-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-2xl my-8\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Queue Item Details</h2><button @click=\"hideQueueItemModal()\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</button></div><div id=\"queue-item-details\"></div><div class=\"flex justify-end space-x-3 mt-6\"><button @click=\"hideQueueItemModal()\" class=\"btn btn-secondary\">Close</button> <button @click=\"retryQueueItem()\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("refresh", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "Retry</button></div></div></div></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+28
-26
@@ -36,7 +36,7 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
|
||||
<title>{ metadata.Title } - Bookhoard Reader</title>
|
||||
<link rel="manifest" href="/static/manifest.json"/>
|
||||
<link href="/static/reader-fonts.css" rel="stylesheet"/>
|
||||
@@ -71,7 +71,7 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="reader-viewport" class="absolute inset-x-0 top-[52px] bottom-[52px]">
|
||||
<div id="reader-viewport" class="absolute inset-x-0" style="top: 56px; bottom: 56px;">
|
||||
<foliate-view id="reader-view" class="block w-full h-full"></foliate-view>
|
||||
</div>
|
||||
@DictionaryPopup()
|
||||
@@ -82,25 +82,25 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress) {
|
||||
<div id="reader-chrome" class="transition-opacity duration-300">
|
||||
<!-- Top bar -->
|
||||
<div class="fixed top-0 left-0 right-0 bg-opacity-95 backdrop-blur border-b z-40" style="background-color: var(--bg-primary);">
|
||||
<div class="flex items-center justify-between px-4 py-3">
|
||||
<a href={ "/media/" + metadata.MediaItemID } class="text-lg hover:underline">
|
||||
<div id="reader-topbar" class="fixed top-0 left-0 right-0 bg-opacity-95 backdrop-blur border-b z-40 pt-[env(safe-area-inset-top)]" style="background-color: var(--bg-primary);">
|
||||
<div class="flex items-center justify-between px-3 py-2 sm:px-4 sm:py-3">
|
||||
<a id="reader-back" href={ "/media/" + metadata.MediaItemID } class="text-base sm:text-lg hover:underline">
|
||||
← Back
|
||||
</a>
|
||||
<h1 class="text-lg font-semibold">{ metadata.Title }</h1>
|
||||
<h1 class="text-base sm:text-lg font-semibold hidden sm:block sm:truncate">{ metadata.Title }</h1>
|
||||
<button
|
||||
@click="toggleSettings()"
|
||||
class="p-2 rounded-lg hover:bg-gray-700"
|
||||
class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700"
|
||||
title="Settings"
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
</div>
|
||||
</div> <!-- Bottom bar -->
|
||||
<div class="fixed bottom-0 left-0 right-0 bg-opacity-95 backdrop-blur border-t z-40" style="background-color: var(--bg-primary);">
|
||||
<div class="flex items-center px-2 py-2 gap-1">
|
||||
<div id="reader-bottombar" class="fixed bottom-0 left-0 right-0 bg-opacity-95 backdrop-blur border-t z-40 pb-[env(safe-area-inset-bottom)]" style="background-color: var(--bg-primary);">
|
||||
<div class="flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1">
|
||||
<!-- Left navigation -->
|
||||
<button @click="goLeft()" class="p-2 rounded-lg hover:bg-gray-700" title="Go Left" aria-label="Go left">
|
||||
<button @click="goLeft()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Left" aria-label="Go left">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 15 6 L 9 12 L 15 18"></path>
|
||||
</svg>
|
||||
@@ -118,7 +118,7 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
/>
|
||||
<datalist id="tick-marks"></datalist>
|
||||
<!-- Right navigation -->
|
||||
<button @click="goRight()" class="p-2 rounded-lg hover:bg-gray-700" title="Go Right" aria-label="Go right">
|
||||
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Right" aria-label="Go right">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 9 6 L 15 12 L 9 18"></path>
|
||||
</svg>
|
||||
@@ -129,7 +129,7 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
<template x-if="isFixedLayout">
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Zoom out -->
|
||||
<button @click="zoomOut()" class="p-2 rounded-lg hover:bg-gray-700" title="Zoom Out" aria-label="Zoom out">
|
||||
<button @click="zoomOut()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom Out" aria-label="Zoom out">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 5 10 L 15 10"></path>
|
||||
</svg>
|
||||
@@ -139,20 +139,20 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
<span x-text="zoomPercent + '%'">100%</span>
|
||||
</button>
|
||||
<!-- Zoom in -->
|
||||
<button @click="zoomIn()" class="p-2 rounded-lg hover:bg-gray-700" title="Zoom In" aria-label="Zoom in">
|
||||
<button @click="zoomIn()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom In" aria-label="Zoom in">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 10 5 L 10 15 M 5 10 L 15 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Magnifier -->
|
||||
<button @click="toggleMagnifier()" class="p-2 rounded-lg hover:bg-gray-700" title="Magnifier" aria-label="Toggle magnifier">
|
||||
<button @click="toggleMagnifier()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Magnifier" aria-label="Toggle magnifier">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<circle cx="9" cy="9" r="5"></circle>
|
||||
<path d="M 13 13 L 18 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Pan/Select mode (PDF only) -->
|
||||
<button x-show="isPDF" @click="toggleInteractionMode()" class="p-2 rounded-lg hover:bg-gray-700" title="Pan/Select Mode" aria-label="Toggle pan/select mode">
|
||||
<button x-show="isPDF" @click="toggleInteractionMode()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Pan/Select Mode" aria-label="Toggle pan/select mode">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 5 3 v 12 M 5 15 l -2 2 M 5 15 l 2 2 M 5 3 l 3 3"></path>
|
||||
</svg>
|
||||
@@ -162,24 +162,26 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
<!-- Separator -->
|
||||
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
||||
<!-- Progress display -->
|
||||
<div id="progress-display" x-text="progressText" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[4rem] text-center cursor-pointer">
|
||||
if progress.FormatGroup == "reflowable" {
|
||||
if metadata.EstimatedPages > 0 {
|
||||
{ fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages) }
|
||||
<div id="progress-display" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[4rem] max-w-[5rem] sm:max-w-none text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
|
||||
<span class="hidden sm:inline" x-text="progressLabel"></span><span x-text="progressMain">
|
||||
if progress.FormatGroup == "reflowable" {
|
||||
if metadata.EstimatedPages > 0 {
|
||||
{ fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages) }
|
||||
} else {
|
||||
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
|
||||
}
|
||||
} else {
|
||||
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
|
||||
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
|
||||
}
|
||||
} else {
|
||||
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Separator -->
|
||||
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-1">
|
||||
<button @click="toggleTOC()" class="p-2 rounded-lg hover:bg-gray-700" title="Table of Contents">📖</button>
|
||||
<button @click="addBookmark()" class="p-2 rounded-lg hover:bg-gray-700" title="Bookmark">🏷️</button>
|
||||
<button @click="toggleBookmarks()" class="p-2 rounded-lg hover:bg-gray-700" title="Notes">📝</button>
|
||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents">📖</button>
|
||||
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark">🏷️</button>
|
||||
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Notes">📝</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+14
-14
@@ -60,7 +60,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\"><title>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div></div><div id=\"reader-viewport\" class=\"absolute inset-x-0 top-[52px] bottom-[52px]\"><foliate-view id=\"reader-view\" class=\"block w-full h-full\"></foliate-view></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div></div><div id=\"reader-viewport\" class=\"absolute inset-x-0\" style=\"top: 56px; bottom: 56px;\"><foliate-view id=\"reader-view\" class=\"block w-full h-full\"></foliate-view></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -163,33 +163,33 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div id=\"reader-chrome\" class=\"transition-opacity duration-300\"><!-- Top bar --><div class=\"fixed top-0 left-0 right-0 bg-opacity-95 backdrop-blur border-b z-40\" style=\"background-color: var(--bg-primary);\"><div class=\"flex items-center justify-between px-4 py-3\"><a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div id=\"reader-chrome\" class=\"transition-opacity duration-300\"><!-- Top bar --><div id=\"reader-topbar\" class=\"fixed top-0 left-0 right-0 bg-opacity-95 backdrop-blur border-b z-40 pt-[env(safe-area-inset-top)]\" style=\"background-color: var(--bg-primary);\"><div class=\"flex items-center justify-between px-3 py-2 sm:px-4 sm:py-3\"><a id=\"reader-back\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 templ.SafeURL
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + metadata.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 87, Col: 46}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 87, Col: 63}
|
||||
}
|
||||
_, 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, "\" class=\"text-lg hover:underline\">← Back</a><h1 class=\"text-lg font-semibold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" class=\"text-base sm:text-lg hover:underline\">← Back</a><h1 class=\"text-base sm:text-lg font-semibold hidden sm:block sm:truncate\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 90, Col: 54}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 90, Col: 95}
|
||||
}
|
||||
_, 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, 12, "</h1><button @click=\"toggleSettings()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Settings\">⚙️</button></div></div><!-- Bottom bar --><div class=\"fixed bottom-0 left-0 right-0 bg-opacity-95 backdrop-blur border-t z-40\" style=\"background-color: var(--bg-primary);\"><div class=\"flex items-center px-2 py-2 gap-1\"><!-- Left navigation --><button @click=\"goLeft()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Go Left\" aria-label=\"Go left\"><svg class=\"reader-icon\" width=\"24\" height=\"24\" aria-hidden=\"true\"><path d=\"M 15 6 L 9 12 L 15 18\"></path></svg></button><!-- Progress slider --><input id=\"progress-slider\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" list=\"tick-marks\" @input=\"goToFraction($event.target.value)\" class=\"grow\"> <datalist id=\"tick-marks\"></datalist><!-- Right navigation --><button @click=\"goRight()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Go Right\" aria-label=\"Go right\"><svg class=\"reader-icon\" width=\"24\" height=\"24\" aria-hidden=\"true\"><path d=\"M 9 6 L 15 12 L 9 18\"></path></svg></button><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Zoom controls (fixed-layout only) --><template x-if=\"isFixedLayout\"><div class=\"flex items-center gap-1\"><!-- Zoom out --><button @click=\"zoomOut()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Zoom Out\" aria-label=\"Zoom out\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 5 10 L 15 10\"></path></svg></button><!-- Zoom percentage --><button @click=\"resetZoom()\" class=\"text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]\" title=\"Reset Zoom\" aria-label=\"Reset zoom\"><span x-text=\"zoomPercent + '%'\">100%</span></button><!-- Zoom in --><button @click=\"zoomIn()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Zoom In\" aria-label=\"Zoom in\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 10 5 L 10 15 M 5 10 L 15 10\"></path></svg></button><!-- Magnifier --><button @click=\"toggleMagnifier()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Magnifier\" aria-label=\"Toggle magnifier\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><circle cx=\"9\" cy=\"9\" r=\"5\"></circle> <path d=\"M 13 13 L 18 18\"></path></svg></button><!-- Pan/Select mode (PDF only) --><button x-show=\"isPDF\" @click=\"toggleInteractionMode()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Pan/Select Mode\" aria-label=\"Toggle pan/select mode\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 5 3 v 12 M 5 15 l -2 2 M 5 15 l 2 2 M 5 3 l 3 3\"></path></svg></button></div></template><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Progress display --><div id=\"progress-display\" x-text=\"progressText\" @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[4rem] text-center cursor-pointer\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h1><button @click=\"toggleSettings()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Settings\">⚙️</button></div></div><!-- Bottom bar --><div id=\"reader-bottombar\" class=\"fixed bottom-0 left-0 right-0 bg-opacity-95 backdrop-blur border-t z-40 pb-[env(safe-area-inset-bottom)]\" style=\"background-color: var(--bg-primary);\"><div class=\"flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1\"><!-- Left navigation --><button @click=\"goLeft()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Go Left\" aria-label=\"Go left\"><svg class=\"reader-icon\" width=\"24\" height=\"24\" aria-hidden=\"true\"><path d=\"M 15 6 L 9 12 L 15 18\"></path></svg></button><!-- Progress slider --><input id=\"progress-slider\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" list=\"tick-marks\" @input=\"goToFraction($event.target.value)\" class=\"grow\"> <datalist id=\"tick-marks\"></datalist><!-- Right navigation --><button @click=\"goRight()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Go Right\" aria-label=\"Go right\"><svg class=\"reader-icon\" width=\"24\" height=\"24\" aria-hidden=\"true\"><path d=\"M 9 6 L 15 12 L 9 18\"></path></svg></button><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Zoom controls (fixed-layout only) --><template x-if=\"isFixedLayout\"><div class=\"flex items-center gap-1\"><!-- Zoom out --><button @click=\"zoomOut()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Zoom Out\" aria-label=\"Zoom out\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 5 10 L 15 10\"></path></svg></button><!-- Zoom percentage --><button @click=\"resetZoom()\" class=\"text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]\" title=\"Reset Zoom\" aria-label=\"Reset zoom\"><span x-text=\"zoomPercent + '%'\">100%</span></button><!-- Zoom in --><button @click=\"zoomIn()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Zoom In\" aria-label=\"Zoom in\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 10 5 L 10 15 M 5 10 L 15 10\"></path></svg></button><!-- Magnifier --><button @click=\"toggleMagnifier()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Magnifier\" aria-label=\"Toggle magnifier\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><circle cx=\"9\" cy=\"9\" r=\"5\"></circle> <path d=\"M 13 13 L 18 18\"></path></svg></button><!-- Pan/Select mode (PDF only) --><button x-show=\"isPDF\" @click=\"toggleInteractionMode()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Pan/Select Mode\" aria-label=\"Toggle pan/select mode\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 5 3 v 12 M 5 15 l -2 2 M 5 15 l 2 2 M 5 3 l 3 3\"></path></svg></button></div></template><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Progress display --><div id=\"progress-display\" @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[4rem] max-w-[5rem] sm:max-w-none text-center cursor-pointer truncate whitespace-nowrap overflow-hidden\"><span class=\"hidden sm:inline\" x-text=\"progressLabel\"></span><span x-text=\"progressMain\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -198,7 +198,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 168, Col: 112}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 169, Col: 113}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -208,7 +208,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%%", progress.Percentage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 170, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 171, Col: 52}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -219,14 +219,14 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 173, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 174, Col: 72}
|
||||
}
|
||||
_, 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, 13, "</div><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Action buttons --><div class=\"flex items-center gap-1\"><button @click=\"toggleTOC()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Table of Contents\">📖</button> <button @click=\"addBookmark()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Bookmark\">🏷️</button> <button @click=\"toggleBookmarks()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Notes\">📝</button></div></div></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span></div><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Action buttons --><div class=\"flex items-center gap-1\"><button @click=\"toggleTOC()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Table of Contents\">📖</button> <button @click=\"addBookmark()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Bookmark\">🏷️</button> <button @click=\"toggleBookmarks()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Notes\">📝</button></div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -359,7 +359,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(bookmark.CfiPosition)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 390, Col: 38}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 392, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -372,7 +372,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 393, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 395, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -385,7 +385,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 395, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 397, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
+68
-59
@@ -6,70 +6,79 @@ templ Register() {
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Register</title>
|
||||
<title>Register - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script type="module" src="/static/main.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body class="theme-tokyo-night min-h-screen" x-data="register" x-init="initRegisterTheme()">
|
||||
<div class="container mx-auto px-4 py-8 max-w-md">
|
||||
<h1 class="text-3xl font-bold text-center mb-8">Register for Bookhoard</h1>
|
||||
<form hx-post="/api/auth/register" hx-target="#result" hx-swap="innerHTML" class="card p-6 rounded-lg shadow-md border">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email</label>
|
||||
<input type="email" id="email" name="email" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Username</label>
|
||||
<input type="text" id="username" name="username" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">First Name</label>
|
||||
<input type="text" name="first_name" placeholder="Optional" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Last Name</label>
|
||||
<input type="text" name="last_name" placeholder="Optional" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"/>
|
||||
</div>
|
||||
<div id="password-requirements" class="mb-4 p-4 rounded-lg border text-sm" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<p class="font-semibold mb-2" style="color: var(--text-primary)">Password Requirements:</p>
|
||||
<ul class="space-y-1" style="color: var(--text-secondary)">
|
||||
<li id="req-length" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> At least 8 characters
|
||||
</li>
|
||||
<li id="req-upper" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> One uppercase letter
|
||||
</li>
|
||||
<li id="req-lower" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> One lowercase letter
|
||||
</li>
|
||||
<li id="req-number" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> One number
|
||||
</li>
|
||||
<li id="req-special" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> One special character
|
||||
</li>
|
||||
<li id="req-match" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> Passwords match
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Password</label>
|
||||
<input type="password" id="password" name="password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm Password</label>
|
||||
<input type="password" id="confirm-password" name="confirm_password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required/>
|
||||
</div>
|
||||
<button type="submit" id="register-btn" class="btn-primary w-full py-2 rounded opacity-50 cursor-not-allowed" disabled>
|
||||
Register
|
||||
</button>
|
||||
</form>
|
||||
<div id="result" class="mt-4 text-center"></div>
|
||||
<p class="text-center mt-4">
|
||||
<a href="/login" class="text-blue-500 hover:underline">Already have an account? Login</a>
|
||||
</p>
|
||||
<div class="brand-gradient min-h-screen flex flex-col items-center justify-center p-4">
|
||||
<div class="w-full max-w-sm">
|
||||
<a href="/" class="flex items-center justify-center gap-2.5 mb-8">
|
||||
<span class="text-3xl">📚</span>
|
||||
<span class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Bookhoard</span>
|
||||
</a>
|
||||
<h1 class="text-xl font-bold text-center mb-6" style="color: var(--text-primary)">Create your account</h1>
|
||||
<form hx-post="/api/auth/register" hx-target="#result" hx-swap="innerHTML" class="card p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Email</label>
|
||||
<input type="email" id="email" name="email" class="input" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Username</label>
|
||||
<input type="text" id="username" name="username" class="input" required/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">First Name</label>
|
||||
<input type="text" name="first_name" placeholder="Optional" class="input"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Last Name</label>
|
||||
<input type="text" name="last_name" placeholder="Optional" class="input"/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="password-requirements" class="p-4 rounded-xl text-sm" style="background-color: var(--bg-primary); border: 1px solid var(--border);">
|
||||
<p class="font-semibold mb-2" style="color: var(--text-primary)">Password Requirements:</p>
|
||||
<ul class="space-y-1" style="color: var(--text-secondary)">
|
||||
<li id="req-length" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> At least 8 characters
|
||||
</li>
|
||||
<li id="req-upper" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> One uppercase letter
|
||||
</li>
|
||||
<li id="req-lower" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> One lowercase letter
|
||||
</li>
|
||||
<li id="req-number" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> One number
|
||||
</li>
|
||||
<li id="req-special" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> One special character
|
||||
</li>
|
||||
<li id="req-match" class="flex items-center gap-2">
|
||||
<span class="requirement-icon">○</span> Passwords match
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Password</label>
|
||||
<input type="password" id="password" name="password" class="input" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Confirm Password</label>
|
||||
<input type="password" id="confirm-password" name="confirm_password" class="input" required/>
|
||||
</div>
|
||||
<button type="submit" id="register-btn" class="btn btn-primary w-full py-2.5 opacity-50 cursor-not-allowed" disabled>
|
||||
Register
|
||||
</button>
|
||||
</form>
|
||||
<div id="result" class="mt-4 text-center text-sm"></div>
|
||||
<p class="text-center mt-6 text-sm" style="color: var(--text-secondary)">
|
||||
Already have an account?
|
||||
<a href="/login" class="font-medium hover:underline" style="color: var(--accent)">Login</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -29,7 +29,7 @@ func Register() templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Register</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night min-h-screen\" x-data=\"register\" x-init=\"initRegisterTheme()\"><div class=\"container mx-auto px-4 py-8 max-w-md\"><h1 class=\"text-3xl font-bold text-center mb-8\">Register for Bookhoard</h1><form hx-post=\"/api/auth/register\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"card p-6 rounded-lg shadow-md border\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Email</label> <input type=\"email\" id=\"email\" name=\"email\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Username</label> <input type=\"text\" id=\"username\" name=\"username\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">First Name</label> <input type=\"text\" name=\"first_name\" placeholder=\"Optional\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Last Name</label> <input type=\"text\" name=\"last_name\" placeholder=\"Optional\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\"></div><div id=\"password-requirements\" class=\"mb-4 p-4 rounded-lg border text-sm\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><p class=\"font-semibold mb-2\" style=\"color: var(--text-primary)\">Password Requirements:</p><ul class=\"space-y-1\" style=\"color: var(--text-secondary)\"><li id=\"req-length\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> At least 8 characters</li><li id=\"req-upper\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One uppercase letter</li><li id=\"req-lower\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One lowercase letter</li><li id=\"req-number\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One number</li><li id=\"req-special\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One special character</li><li id=\"req-match\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> Passwords match</li></ul></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" id=\"password\" name=\"password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Confirm Password</label> <input type=\"password\" id=\"confirm-password\" name=\"confirm_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><button type=\"submit\" id=\"register-btn\" class=\"btn-primary w-full py-2 rounded opacity-50 cursor-not-allowed\" disabled>Register</button></form><div id=\"result\" class=\"mt-4 text-center\"></div><p class=\"text-center mt-4\"><a href=\"/login\" class=\"text-blue-500 hover:underline\">Already have an account? Login</a></p></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Register - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night min-h-screen\" x-data=\"register\" x-init=\"initRegisterTheme()\"><div class=\"brand-gradient min-h-screen flex flex-col items-center justify-center p-4\"><div class=\"w-full max-w-sm\"><a href=\"/\" class=\"flex items-center justify-center gap-2.5 mb-8\"><span class=\"text-3xl\">📚</span> <span class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span></a><h1 class=\"text-xl font-bold text-center mb-6\" style=\"color: var(--text-primary)\">Create your account</h1><form hx-post=\"/api/auth/register\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"card p-6 space-y-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Email</label> <input type=\"email\" id=\"email\" name=\"email\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Username</label> <input type=\"text\" id=\"username\" name=\"username\" class=\"input\" required></div><div class=\"grid grid-cols-2 gap-3\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">First Name</label> <input type=\"text\" name=\"first_name\" placeholder=\"Optional\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Last Name</label> <input type=\"text\" name=\"last_name\" placeholder=\"Optional\" class=\"input\"></div></div><div id=\"password-requirements\" class=\"p-4 rounded-xl text-sm\" style=\"background-color: var(--bg-primary); border: 1px solid var(--border);\"><p class=\"font-semibold mb-2\" style=\"color: var(--text-primary)\">Password Requirements:</p><ul class=\"space-y-1\" style=\"color: var(--text-secondary)\"><li id=\"req-length\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> At least 8 characters</li><li id=\"req-upper\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One uppercase letter</li><li id=\"req-lower\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One lowercase letter</li><li id=\"req-number\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One number</li><li id=\"req-special\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One special character</li><li id=\"req-match\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> Passwords match</li></ul></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" id=\"password\" name=\"password\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Confirm Password</label> <input type=\"password\" id=\"confirm-password\" name=\"confirm_password\" class=\"input\" required></div><button type=\"submit\" id=\"register-btn\" class=\"btn btn-primary w-full py-2.5 opacity-50 cursor-not-allowed\" disabled>Register</button></form><div id=\"result\" class=\"mt-4 text-center text-sm\"></div><p class=\"text-center mt-6 text-sm\" style=\"color: var(--text-secondary)\">Already have an account? <a href=\"/login\" class=\"font-medium hover:underline\" style=\"color: var(--accent)\">Login</a></p></div></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
package templates
|
||||
|
||||
templ RestoreSystemCollectionModal() {
|
||||
<div x-data="collections" class="fixed inset-0 z-50 flex items-center justify-center bg-black/70">
|
||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div x-data="collections" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Restore System Collection</h2>
|
||||
<button type="button" @click="closeCollectionModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("refresh", "h-5 w-5")
|
||||
</span>
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Restore System Collection</h2>
|
||||
</div>
|
||||
<button type="button" @click="closeCollectionModal()" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<p class="mb-4" style="color: var(--text-secondary)">Select a system collection to restore:</p>
|
||||
@@ -13,7 +20,7 @@ templ RestoreSystemCollectionModal() {
|
||||
hx-post="/api/dashboard/restore-system-collection"
|
||||
hx-redirect="/collections"
|
||||
>
|
||||
<select name="collection_name" class="w-full px-4 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
||||
<select name="collection_name" class="input">
|
||||
<option value="">Select collection...</option>
|
||||
<option value="Continue Reading">Continue Reading</option>
|
||||
<option value="Recently Added">Recently Added</option>
|
||||
@@ -22,8 +29,8 @@ templ RestoreSystemCollectionModal() {
|
||||
<option value="Continue Series">Continue Series</option>
|
||||
</select>
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button type="button" @click="closeCollectionModal()" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">Restore</button>
|
||||
<button type="button" @click="closeCollectionModal()" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Restore</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,23 @@ func RestoreSystemCollectionModal() templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"collections\" class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/70\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Restore System Collection</h2><button type=\"button\" @click=\"closeCollectionModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><div class=\"mb-4\"><p class=\"mb-4\" style=\"color: var(--text-secondary)\">Select a system collection to restore:</p><form hx-post=\"/api/dashboard/restore-system-collection\" hx-redirect=\"/collections\"><select name=\"collection_name\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"\">Select collection...</option> <option value=\"Continue Reading\">Continue Reading</option> <option value=\"Recently Added\">Recently Added</option> <option value=\"Recently Read\">Recently Read</option> <option value=\"Not Started\">Not Started</option> <option value=\"Continue Series\">Continue Series</option></select><div class=\"flex justify-end space-x-3 mt-6\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Restore</button></div></form></div></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"collections\" class=\"fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("refresh", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Restore System Collection</h2></div><button type=\"button\" @click=\"closeCollectionModal()\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</button></div><div class=\"mb-4\"><p class=\"mb-4\" style=\"color: var(--text-secondary)\">Select a system collection to restore:</p><form hx-post=\"/api/dashboard/restore-system-collection\" hx-redirect=\"/collections\"><select name=\"collection_name\" class=\"input\"><option value=\"\">Select collection...</option> <option value=\"Continue Reading\">Continue Reading</option> <option value=\"Recently Added\">Recently Added</option> <option value=\"Recently Read\">Recently Read</option> <option value=\"Not Started\">Not Started</option> <option value=\"Continue Series\">Continue Series</option></select><div class=\"flex justify-end space-x-3 mt-6\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">Restore</button></div></form></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+26
-18
@@ -17,15 +17,24 @@ templ Series(user User, seriesList []SeriesCardData, libData []LibraryData, curr
|
||||
<body x-data="seriesPage" x-init="initSeriesPage()" class="theme-{ user.Theme }">
|
||||
@Header(user, "/series")
|
||||
@LibrarySwitcher(libData, currentLibraryID)
|
||||
<!-- Series Grid -->
|
||||
<div id="series-container">
|
||||
<main class="w-full px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-6" style="color: var(--text-primary)">Series</h1>
|
||||
<main class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-6 flex items-center gap-3">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("layers", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Series</h1>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Grouped by series metadata</p>
|
||||
</div>
|
||||
</div>
|
||||
if len(seriesList) == 0 {
|
||||
<div id="series-empty" class="text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">📚</div>
|
||||
<div id="series-empty" class="card text-center py-16 px-6">
|
||||
<div class="grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("layers", "h-7 w-7")
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Series Found</h3>
|
||||
<p>Books with series metadata will appear here</p>
|
||||
<p style="color: var(--text-secondary)">Books with series metadata will appear here</p>
|
||||
</div>
|
||||
}
|
||||
<div id="series-grid" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||
@@ -33,28 +42,27 @@ templ Series(user User, seriesList []SeriesCardData, libData []LibraryData, curr
|
||||
@SeriesCard(series)
|
||||
}
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
if totalPages > 1 {
|
||||
<div id="series-pagination" class="flex justify-center items-center gap-4 mt-8">
|
||||
<div id="series-pagination" class="flex justify-center items-center gap-3 mt-10">
|
||||
if currentPage > 1 {
|
||||
<a
|
||||
href={ fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage-1) }
|
||||
class="px-4 py-2 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
← Previous
|
||||
@Icon("chevron-left", "h-4 w-4")
|
||||
<span>Previous</span>
|
||||
</a>
|
||||
}
|
||||
<span class="text-sm" style="color: var(--text-secondary)">
|
||||
<span class="chip">
|
||||
Page { fmt.Sprintf("%d", currentPage) } of { fmt.Sprintf("%d", totalPages) }
|
||||
</span>
|
||||
if currentPage < totalPages {
|
||||
<a
|
||||
href={ fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage+1) }
|
||||
class="px-4 py-2 rounded-lg border"
|
||||
style="border-color: var(--border); color: var(--text-primary);"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Next →
|
||||
<span>Next</span>
|
||||
@Icon("chevron-right", "h-4 w-4")
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
@@ -68,7 +76,7 @@ templ Series(user User, seriesList []SeriesCardData, libData []LibraryData, curr
|
||||
|
||||
templ SeriesCard(series SeriesCardData) {
|
||||
<a href={ "/series/detail?name=" + url.QueryEscape(series.Name) } class="block">
|
||||
<div class="series-card rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow" style="background-color: var(--bg-secondary);">
|
||||
<div class="series-card card rounded-2xl overflow-hidden cursor-pointer" style="box-shadow: var(--shadow-card);">
|
||||
<div class={ "stacked-covers cover-count-" + fmt.Sprintf("%d", len(series.CoverPaths)) }>
|
||||
for i, cover := range series.CoverPaths {
|
||||
<img
|
||||
@@ -80,8 +88,8 @@ templ SeriesCard(series SeriesCardData) {
|
||||
/>
|
||||
}
|
||||
if len(series.CoverPaths) == 0 {
|
||||
<div class="cover-img cover-0 flex items-center justify-center" style="background: var(--bg-primary);">
|
||||
<span class="text-3xl">📚</span>
|
||||
<div class="cover-img cover-0 flex items-center justify-center" style="background: var(--bg-primary); color: var(--text-secondary);">
|
||||
@Icon("book", "h-8 w-8 opacity-60")
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
+78
-38
@@ -46,17 +46,33 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Series Grid --><div id=\"series-container\"><main class=\"w-full px-4 py-8\"><h1 class=\"text-3xl font-bold mb-6\" style=\"color: var(--text-primary)\">Series</h1>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div id=\"series-container\"><main class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-6 flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("layers", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><div><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Series</h1><p class=\"text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Grouped by series metadata</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(seriesList) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div id=\"series-empty\" class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📚</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Series Found</h3><p>Books with series metadata will appear here</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div id=\"series-empty\" class=\"card text-center py-16 px-6\"><div class=\"grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("layers", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Series Found</h3><p style=\"color: var(--text-secondary)\">Books with series metadata will appear here</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div id=\"series-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div id=\"series-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -66,89 +82,105 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><!-- Pagination -->")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if totalPages > 1 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div id=\"series-pagination\" class=\"flex justify-center items-center gap-4 mt-8\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div id=\"series-pagination\" class=\"flex justify-center items-center gap-3 mt-10\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if currentPage > 1 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage-1))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 41, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 49, Col: 93}
|
||||
}
|
||||
_, 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, 8, "\" class=\"px-4 py-2 rounded-lg border\" style=\"border-color: var(--border); color: var(--text-primary);\">← Previous</a> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"btn btn-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("chevron-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span>Previous</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"text-sm\" style=\"color: var(--text-secondary)\">Page ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"chip\">Page ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", currentPage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 49, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 57, Col: 45}
|
||||
}
|
||||
_, 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, 10, " of ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " of ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", totalPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 49, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 57, Col: 82}
|
||||
}
|
||||
_, 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, 11, "</span> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if currentPage < totalPages {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 templ.SafeURL
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage+1))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 53, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 61, Col: 93}
|
||||
}
|
||||
_, 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, 13, "\" class=\"px-4 py-2 rounded-lg border\" style=\"border-color: var(--border); color: var(--text-primary);\">Next →</a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" class=\"btn btn-secondary\"><span>Next</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("chevron-right", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</main></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</main></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -156,7 +188,7 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -185,20 +217,20 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
templ_7745c5c3_Var6 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 templ.SafeURL
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs("/series/detail?name=" + url.QueryEscape(series.Name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 70, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 78, Col: 64}
|
||||
}
|
||||
_, 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, 18, "\" class=\"block\"><div class=\"series-card rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow\" style=\"background-color: var(--bg-secondary);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" class=\"block\"><div class=\"series-card card rounded-2xl overflow-hidden cursor-pointer\" style=\"box-shadow: var(--shadow-card);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -207,7 +239,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -220,7 +252,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -230,20 +262,20 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<img src=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(cover)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 75, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 83, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -256,70 +288,78 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" alt=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" alt=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(series.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 77, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 85, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\"> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\"> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if len(series.CoverPaths) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"cover-img cover-0 flex items-center justify-center\" style=\"background: var(--bg-primary);\"><span class=\"text-3xl\">📚</span></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"cover-img cover-0 flex items-center justify-center\" style=\"background: var(--bg-primary); color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("book", "h-8 w-8 opacity-60").Render(ctx, templ_7745c5c3_Buffer)
|
||||
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, 26, "</div><div class=\"series-card-info px-3 py-2\"><h3 class=\"font-semibold text-sm line-clamp-2\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div><div class=\"series-card-info px-3 py-2\"><h3 class=\"font-semibold text-sm line-clamp-2\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(series.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 89, Col: 99}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 97, Col: 99}
|
||||
}
|
||||
_, 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, 27, "</h3><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</h3><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.BookCount))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 91, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 99, Col: 42}
|
||||
}
|
||||
_, 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, 28, " of ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " of ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.TotalInSeries))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 91, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 99, Col: 89}
|
||||
}
|
||||
_, 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, 29, " books</p></div></div></a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " books</p></div></div></a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+113
-95
@@ -12,21 +12,24 @@ templ Setup() {
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body class="theme-tokyo-night min-h-screen" x-data="setupWizard" x-init="initSetup()">
|
||||
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
||||
<div class="mx-auto container px-4 py-8 max-w-2xl">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold mb-2" style="color: var(--text-primary)">Welcome to Bookhoard</h1>
|
||||
<span class="inline-grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("library", "h-7 w-7")
|
||||
</span>
|
||||
<h1 class="text-4xl font-bold mb-2 tracking-tight" style="color: var(--text-primary)">Welcome to Bookhoard</h1>
|
||||
<p class="text-lg" style="color: var(--text-secondary)">Let's get your library set up.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step indicator -->
|
||||
<div class="flex items-center justify-center mb-8 gap-2">
|
||||
<template x-for="n in 4" :key="n">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all"
|
||||
class="w-10 h-10 rounded-full grid place-items-center font-bold text-sm transition-all"
|
||||
:class="currentStep >= n ? 'bg-[var(--accent)] text-[var(--bg-primary)]' : 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border)]'"
|
||||
>
|
||||
<span x-text="currentStep > n ? '✓' : n"></span>
|
||||
<template x-if="currentStep > n">@Icon("check", "h-5 w-5")</template>
|
||||
<span x-show="currentStep <= n" x-text="n"></span>
|
||||
</div>
|
||||
<div
|
||||
x-text="['Admin', 'Libraries', 'Folders', 'Scan'][n-1]"
|
||||
@@ -38,76 +41,73 @@ templ Setup() {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Error display -->
|
||||
<div x-show="errorMsg" class="mb-4 p-4 rounded-lg border border-red-500/50 bg-red-500/10 text-red-400 text-sm">
|
||||
<div x-show="errorMsg" class="mb-4 card p-4 text-sm" style="border-color: var(--status-danger); background-color: color-mix(in srgb, var(--status-danger) 12%, transparent); color: var(--status-danger);">
|
||||
<span x-text="errorMsg"></span>
|
||||
</div>
|
||||
|
||||
<!-- ==================== STEP 1: Create Admin ==================== -->
|
||||
<div x-show="currentStep === 1" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<div x-show="currentStep === 1" class="card p-6">
|
||||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Create Administrator Account</h2>
|
||||
<p class="mb-6" style="color: var(--text-secondary)">This first account will have full admin privileges.</p>
|
||||
|
||||
<form @submit.prevent="submitAdmin()">
|
||||
<div class="mb-6 p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Server URL (Base URL)</label>
|
||||
<div class="mb-6 card p-4" style="background-color: var(--bg-primary);">
|
||||
<label for="base-url" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Server URL (Base URL)</label>
|
||||
<input
|
||||
type="url"
|
||||
id="base-url"
|
||||
x-model="admin.baseUrl"
|
||||
placeholder="https://books.example.com"
|
||||
class="w-full px-3 py-2 border rounded mt-2"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
|
||||
class="input mt-2"
|
||||
required
|
||||
/>
|
||||
<p class="text-sm mt-2" style="color: var(--text-secondary)">The public URL of your Bookhoard instance. Used for device sync, OPDS feed, and API endpoints.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email</label>
|
||||
<label for="email" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
x-model="admin.email"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
|
||||
class="input mt-2"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Username</label>
|
||||
<label for="username" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="admin.username"
|
||||
id="username"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
|
||||
class="input mt-2"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">First Name</label>
|
||||
<label for="first-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="first-name"
|
||||
x-model="admin.firstName"
|
||||
placeholder="Optional"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
|
||||
class="input mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Last Name</label>
|
||||
<label for="last-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="last-name"
|
||||
x-model="admin.lastName"
|
||||
placeholder="Optional"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
|
||||
class="input mt-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="password-requirements" class="mb-4 p-4 rounded-lg border text-sm" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<div id="password-requirements" class="mb-4 card p-4 text-sm" style="background-color: var(--bg-primary);">
|
||||
<p class="font-semibold mb-2" style="color: var(--text-primary)">Password Requirements:</p>
|
||||
<ul class="space-y-1" style="color: var(--text-secondary)">
|
||||
<li id="req-length" class="flex items-center gap-2">
|
||||
@@ -131,48 +131,49 @@ templ Setup() {
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Password</label>
|
||||
<label for="password" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
x-model="admin.password"
|
||||
id="password"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
|
||||
class="input mt-2"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm Password</label>
|
||||
<label for="confirm-password" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Confirm Password</label>
|
||||
<input
|
||||
type="password"
|
||||
x-model="admin.confirmPassword"
|
||||
id="confirm-password"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
|
||||
class="input mt-2"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
id="register-btn"
|
||||
class="btn-primary w-full py-2 rounded opacity-50 cursor-not-allowed"
|
||||
class="btn btn-primary w-full"
|
||||
:disabled="loading"
|
||||
x-text="loading ? 'Creating...' : 'Create Admin Account'"
|
||||
x-text="loading ? 'Creating…' : 'Create Admin Account'"
|
||||
></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- ==================== STEP 2: Create Libraries ==================== -->
|
||||
<div x-show="currentStep === 2" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<div x-show="currentStep === 2" class="card p-6">
|
||||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Create Libraries</h2>
|
||||
<p class="mb-6" style="color: var(--text-secondary)">Add one or more media libraries. You can always add more later.</p>
|
||||
|
||||
<!-- Created libraries list -->
|
||||
<div x-show="libraries.length > 0" class="mb-6 space-y-2">
|
||||
<template x-for="(lib, idx) in libraries" :key="lib.id">
|
||||
<div class="p-3 rounded-lg border flex items-center justify-between" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<div class="card p-3 flex items-center justify-between" style="background-color: var(--bg-primary);">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span>
|
||||
<span class="grid place-items-center h-9 w-9 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
<span class="inline-flex" x-show="lib.type === 'ebooks'">@Icon("book", "h-5 w-5")</span>
|
||||
<span class="inline-flex" x-show="lib.type === 'comics'">@Icon("book-open", "h-5 w-5")</span>
|
||||
<span class="inline-flex" x-show="lib.type === 'manga'">@Icon("library", "h-5 w-5")</span>
|
||||
</span>
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--text-primary)" x-text="lib.name"></p>
|
||||
<p class="text-xs capitalize" style="color: var(--text-secondary)" x-text="lib.type"></p>
|
||||
@@ -181,65 +182,70 @@ templ Setup() {
|
||||
<button
|
||||
type="button"
|
||||
@click="removeLibrary(idx)"
|
||||
class="text-xs text-red-500 hover:underline"
|
||||
>Remove</button>
|
||||
class="icon-btn"
|
||||
style="color: var(--status-danger);"
|
||||
aria-label="Remove library"
|
||||
>@Icon("trash", "h-4 w-4")</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Add library form -->
|
||||
<form @submit.prevent="addLibrary()" class="space-y-4 p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<form @submit.prevent="addLibrary()" class="space-y-4 card p-4" style="background-color: var(--bg-primary);">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Library Name</label>
|
||||
<label for="lib-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Library Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lib-name"
|
||||
x-model="newLibrary.name"
|
||||
placeholder="My Ebook Library"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
|
||||
class="input mt-2"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Library Type</label>
|
||||
<label for="lib-type" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Library Type</label>
|
||||
<select
|
||||
id="lib-type"
|
||||
x-model="newLibrary.type"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
|
||||
class="input mt-2"
|
||||
>
|
||||
<option value="ebooks">📚 Ebooks</option>
|
||||
<option value="comics">📖 Comics</option>
|
||||
<option value="manga">🗾 Manga</option>
|
||||
<option value="ebooks">Ebooks</option>
|
||||
<option value="comics">Comics</option>
|
||||
<option value="manga">Manga</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Description (optional)</label>
|
||||
<label for="lib-desc" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Description (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lib-desc"
|
||||
x-model="newLibrary.description"
|
||||
placeholder="Optional"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
|
||||
class="input mt-2"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-secondary w-full py-2 rounded"
|
||||
class="btn btn-secondary w-full"
|
||||
:disabled="loading || !newLibrary.name.trim()"
|
||||
x-text="loading ? 'Creating...' : '+ Add This Library'"
|
||||
x-text="loading ? 'Creating…' : '+ Add This Library'"
|
||||
></button>
|
||||
</form>
|
||||
|
||||
<div class="flex justify-between mt-6">
|
||||
<button type="button" @click="currentStep = 1" class="btn-secondary px-4 py-2 rounded">Back</button>
|
||||
<button type="button" @click="currentStep = 3" class="btn-primary px-6 py-2 rounded">
|
||||
<button type="button" @click="currentStep = 1" class="btn btn-secondary">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
Back
|
||||
</button>
|
||||
<button type="button" @click="currentStep = 3" class="btn btn-primary">
|
||||
<span x-text="libraries.length > 0 ? 'Next: Configure Folders' : 'Skip – Continue'"></span>
|
||||
@Icon("arrow-right", "h-4 w-4")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== STEP 3: Configure Folders ==================== -->
|
||||
<div x-show="currentStep === 3" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<div x-show="currentStep === 3" class="card p-6">
|
||||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Configure Library Folders</h2>
|
||||
<p class="mb-6" style="color: var(--text-secondary)">Add the filesystem folders where your media files are stored.</p>
|
||||
|
||||
@@ -249,40 +255,44 @@ templ Setup() {
|
||||
|
||||
<div class="space-y-6">
|
||||
<template x-for="lib in libraries" :key="lib.id">
|
||||
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<div class="card p-4" style="background-color: var(--bg-primary);">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span>
|
||||
<span class="grid place-items-center h-8 w-8 rounded-lg" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
<span class="inline-flex" x-show="lib.type === 'ebooks'">@Icon("book", "h-4 w-4")</span>
|
||||
<span class="inline-flex" x-show="lib.type === 'comics'">@Icon("book-open", "h-4 w-4")</span>
|
||||
<span class="inline-flex" x-show="lib.type === 'manga'">@Icon("library", "h-4 w-4")</span>
|
||||
</span>
|
||||
<h4 class="font-semibold" style="color: var(--text-primary)" x-text="lib.name"></h4>
|
||||
</div>
|
||||
|
||||
<!-- Existing folders for this library -->
|
||||
<div class="space-y-1 mb-3" x-show="lib.folders && lib.folders.length > 0">
|
||||
<template x-for="(folder, fIdx) in (lib.folders || [])" :key="folder">
|
||||
<div class="flex items-center justify-between p-2 rounded text-sm" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<span style="color: var(--text-primary)" x-text="folder"></span>
|
||||
<button type="button" @click="removeFolder(lib.id, folder, fIdx)" class="text-xs text-red-500 hover:underline">Remove</button>
|
||||
<div class="flex items-center justify-between p-2 rounded-lg text-sm" style="background-color: var(--bg-secondary);">
|
||||
<span class="flex items-center gap-2 truncate" style="color: var(--text-primary)">
|
||||
@Icon("folder", "h-4 w-4 shrink-0")
|
||||
<span class="truncate" x-text="folder"></span>
|
||||
</span>
|
||||
<button type="button" @click="removeFolder(lib.id, folder, fIdx)" class="icon-btn shrink-0" style="color: var(--status-danger);" aria-label="Remove folder">@Icon("trash", "h-4 w-4")</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Add folder -->
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="'/path/to/' + lib.type"
|
||||
:id="'folderInput-' + lib.id"
|
||||
class="flex-1 px-3 py-2 border rounded text-sm"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
|
||||
class="input flex-1 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="showFolderBrowser('folderInput-' + lib.id)"
|
||||
class="btn-secondary px-3 py-2 text-xs rounded"
|
||||
class="btn btn-secondary px-3 text-xs"
|
||||
>Browse</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="addFolderToLibrary(lib)"
|
||||
class="btn-primary px-3 py-2 text-xs rounded"
|
||||
class="btn btn-primary px-3 text-xs"
|
||||
>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -290,35 +300,40 @@ templ Setup() {
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-6">
|
||||
<button type="button" @click="currentStep = 2" class="btn-secondary px-4 py-2 rounded">Back</button>
|
||||
<button type="button" @click="currentStep = 4" class="btn-primary px-6 py-2 rounded">Next: Scan</button>
|
||||
<button type="button" @click="currentStep = 2" class="btn btn-secondary">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
Back
|
||||
</button>
|
||||
<button type="button" @click="currentStep = 4" class="btn btn-primary">
|
||||
Next: Scan
|
||||
@Icon("arrow-right", "h-4 w-4")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== STEP 4: Scan & Finish ==================== -->
|
||||
<div x-show="currentStep === 4" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<div x-show="currentStep === 4" class="card p-6">
|
||||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Scan & Finish</h2>
|
||||
<p class="mb-6" style="color: var(--text-secondary)">Review your configuration and start the initial scan.</p>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="space-y-3 mb-6">
|
||||
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<p class="text-sm font-medium mb-1" style="color: var(--text-secondary)">Server URL</p>
|
||||
<div class="card p-4" style="background-color: var(--bg-primary);">
|
||||
<p class="text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Server URL</p>
|
||||
<p style="color: var(--text-primary)" x-text="admin.baseUrl"></p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<p class="text-sm font-medium mb-1" style="color: var(--text-secondary)">Administrator</p>
|
||||
<div class="card p-4" style="background-color: var(--bg-primary);">
|
||||
<p class="text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Administrator</p>
|
||||
<p style="color: var(--text-primary)" x-text="admin.username + ' (' + admin.email + ')'"></p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<p class="text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||||
<div class="card p-4" style="background-color: var(--bg-primary);">
|
||||
<p class="text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">
|
||||
Libraries (<span x-text="libraries.length"></span>)
|
||||
</p>
|
||||
<template x-for="lib in libraries" :key="lib.id">
|
||||
<div class="flex items-start justify-between text-sm py-1">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span style="color: var(--text-primary)" x-text="lib.name"></span>
|
||||
<span class="ml-2 text-xs px-2 py-0.5 rounded" style="background-color: var(--accent); color: var(--bg-primary)" x-text="lib.type"></span>
|
||||
<span class="chip" x-text="lib.type"></span>
|
||||
</div>
|
||||
<div class="text-right" style="color: var(--text-secondary)">
|
||||
<span x-text="(lib.folders || []).length + ' folder(s)'"></span>
|
||||
@@ -328,12 +343,11 @@ templ Setup() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scan progress -->
|
||||
<div x-show="scanStarted" class="mb-6 space-y-3">
|
||||
<h3 class="font-semibold" style="color: var(--text-primary)">Scan Progress</h3>
|
||||
<div id="library-progress-list" class="space-y-3">
|
||||
<template x-for="job in scanJobs" :key="job.jobId">
|
||||
<div class="p-3 rounded border" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<div class="card p-3" style="background-color: var(--bg-primary);">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-medium" style="color: var(--text-primary)" x-text="job.libraryName"></span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)" x-text="job.statusText"></span>
|
||||
@@ -347,28 +361,34 @@ templ Setup() {
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div x-show="scanComplete" class="p-4 rounded-lg border border-green-500/50 bg-green-500/10 text-green-400 text-sm">
|
||||
<div x-show="scanComplete" class="card p-4 text-sm" style="border-color: var(--status-success); background-color: color-mix(in srgb, var(--status-success) 12%, transparent); color: var(--status-success);">
|
||||
Initial scan complete! Your libraries are ready to use.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-6">
|
||||
<button type="button" @click="currentStep = 3" class="btn-secondary px-4 py-2 rounded" :disabled="loading">Back</button>
|
||||
<button type="button" @click="currentStep = 3" class="btn btn-secondary" :disabled="loading">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
Back
|
||||
</button>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
x-show="!scanStarted"
|
||||
type="button"
|
||||
@click="startScan()"
|
||||
class="btn-primary px-6 py-2 rounded"
|
||||
class="btn btn-primary"
|
||||
:disabled="loading"
|
||||
>Start Scan</button>
|
||||
>
|
||||
@Icon("sync", "h-4 w-4")
|
||||
Start Scan
|
||||
</button>
|
||||
|
||||
<button
|
||||
x-show="scanStarted && !scanComplete"
|
||||
type="button"
|
||||
@click="finishSetup()"
|
||||
class="btn-secondary px-6 py-2 rounded"
|
||||
class="btn btn-secondary"
|
||||
:disabled="loading"
|
||||
>Skip to Dashboard</button>
|
||||
|
||||
@@ -376,24 +396,22 @@ templ Setup() {
|
||||
x-show="scanComplete"
|
||||
type="button"
|
||||
@click="finishSetup()"
|
||||
class="btn-primary px-6 py-2 rounded"
|
||||
class="btn btn-primary"
|
||||
:disabled="loading"
|
||||
x-text="loading ? 'Finishing...' : 'Go to Dashboard'"
|
||||
x-text="loading ? 'Finishing…' : 'Go to Dashboard'"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Folder Browser Modal (reused from admin library) -->
|
||||
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7)">
|
||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md mx-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
|
||||
<button type="button" @click="hideFolderBrowser()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<button type="button" @click="hideFolderBrowser()" class="icon-btn" aria-label="Close">@Icon("close", "h-5 w-5")</button>
|
||||
</div>
|
||||
<div id="folder-browser-content">
|
||||
<!-- Directory listings rendered by Alpine -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+145
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,37 @@
|
||||
package templates
|
||||
|
||||
// ThemeOption describes a selectable color theme shown in the header picker.
|
||||
type ThemeOption struct {
|
||||
Name string // CSS theme suffix applied as class theme-<name>
|
||||
Label string // Human-readable label
|
||||
Color string // Hex swatch colour
|
||||
}
|
||||
|
||||
// ThemeOptions is the full set of selectable colour themes, in display order.
|
||||
var ThemeOptions = []ThemeOption{
|
||||
{"tokyo-night", "Tokyo Night", "#7aa2f7"},
|
||||
{"dracula", "Dracula", "#bd93f9"},
|
||||
{"nord", "Nord", "#88c0d0"},
|
||||
{"solarized-dark", "Solarized Dark", "#2aa198"},
|
||||
{"monokai", "Monokai", "#a6e22e"},
|
||||
{"one-dark-pro", "One Dark Pro", "#61dafb"},
|
||||
{"material-dark", "Material Dark", "#80cbc4"},
|
||||
{"catppuccin-mocha", "Catppuccin Mocha", "#f38ba8"},
|
||||
{"catppuccin-macchiato", "Catppuccin Macchiato", "#c6a0f6"},
|
||||
{"catppuccin-frappe", "Catppuccin Frappé", "#ca9ee6"},
|
||||
{"catppuccin-latte", "Catppuccin Latte", "#d20f39"},
|
||||
}
|
||||
|
||||
// WoodOption describes a selectable bookshelf background texture.
|
||||
type WoodOption struct {
|
||||
Name string // value stored as data-wood and CSS bg-<name>
|
||||
Label string
|
||||
}
|
||||
|
||||
// WoodOptions is the full set of bookshelf background options.
|
||||
var WoodOptions = []WoodOption{
|
||||
{"none", "None"},
|
||||
{"wood-light", "Wood Light"},
|
||||
{"wood-dark", "Wood Dark"},
|
||||
{"wood-mahogany", "Wood Mahogany"},
|
||||
}
|
||||
@@ -30,6 +30,7 @@ type CollectionData struct {
|
||||
Description string
|
||||
Color string
|
||||
Icon string
|
||||
IsSystem bool
|
||||
}
|
||||
|
||||
type LibraryData struct {
|
||||
|
||||
@@ -12,107 +12,118 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }" x-data="unlinkedBooks" x-init="setupEventDelegation()">
|
||||
@Header(user, "/unlinked")
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<main class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Unlinked Books</h1>
|
||||
<p style="color: var(--text-secondary)">Resolve books that couldn't be automatically matched between devices</p>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("sync", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Unlinked Books</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Resolve books that couldn't be automatically matched between devices</p>
|
||||
</div>
|
||||
if len(unlinkedBooks) > 0 {
|
||||
<div class="flex justify-between items-center mb-6 p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="card p-4 mb-6 flex flex-wrap justify-between items-center gap-3">
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="checkbox" id="select-all-unlinked" @change="toggleAllUnlinked()" class="w-5 h-5"/>
|
||||
<label for="select-all-unlinked" class="cursor-pointer" style="color: var(--text-primary)">Select All</label>
|
||||
<label for="select-all-unlinked" class="cursor-pointer text-sm font-medium" style="color: var(--text-primary)">Select All</label>
|
||||
<span id="selected-count" class="text-sm" style="color: var(--text-secondary)">0 selected</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button @click="bulkAutoLink()" class="btn-secondary px-4 py-2 rounded-lg text-sm">
|
||||
🤖 Auto-Link Selected (High Confidence)
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button @click="bulkAutoLink()" class="btn btn-secondary text-sm">
|
||||
@Icon("sync", "h-4 w-4")
|
||||
Auto-Link Selected (High Confidence)
|
||||
</button>
|
||||
<button @click="bulkGetSuggestions()" class="btn-secondary px-4 py-2 rounded-lg text-sm">
|
||||
💡 Get Suggestions for Selected
|
||||
<button @click="bulkGetSuggestions()" class="btn btn-secondary text-sm">
|
||||
@Icon("search", "h-4 w-4")
|
||||
Get Suggestions for Selected
|
||||
</button>
|
||||
<button @click="showBulkManualLink()" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
||||
🔗 Bulk Manual Link
|
||||
<button @click="showBulkManualLink()" class="btn btn-primary text-sm">
|
||||
@Icon("edit", "h-4 w-4")
|
||||
Bulk Manual Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div id="unlinked-container" class="space-y-6">
|
||||
if len(unlinkedBooks) == 0 {
|
||||
<div class="text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="text-6xl mb-4">✓</div>
|
||||
<div class="card text-center py-16">
|
||||
<span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: color-mix(in srgb, var(--status-success) 16%, transparent); color: var(--status-success);">
|
||||
@Icon("check-circle", "h-7 w-7")
|
||||
</span>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">All Books Linked!</h3>
|
||||
<p>There are no unlinked books that need manual resolution.</p>
|
||||
<p style="color: var(--text-secondary)">There are no unlinked books that need manual resolution.</p>
|
||||
</div>
|
||||
}
|
||||
for _, book := range unlinkedBooks {
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="card p-6">
|
||||
<div class="flex gap-6">
|
||||
<div class="flex items-start pt-2">
|
||||
<input type="checkbox" class="unlinked-checkbox w-5 h-5" data-progress-id="{ book.ProgressID }" data-title="{ book.TitleFromDevice }" onchange="updateSelectedCount()"/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="text-4xl">
|
||||
<span class="grid place-items-center h-11 w-11 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
if book.DeviceType == "koreader" {
|
||||
📖
|
||||
@Icon("book-open", "h-5 w-5")
|
||||
} else if book.DeviceType == "kobo" {
|
||||
📚
|
||||
@Icon("library", "h-5 w-5")
|
||||
} else {
|
||||
📱
|
||||
@Icon("device", "h-5 w-5")
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg" style="color: var(--text-primary)">{ book.TitleFromDevice }</h3>
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<h3 class="font-semibold text-lg truncate" style="color: var(--text-primary)">{ book.TitleFromDevice }</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">
|
||||
Device: { book.DeviceName } ({ book.DeviceType })
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2 text-sm mb-4">
|
||||
<div>
|
||||
<span style="color: var(--text-secondary)">File Path:</span>
|
||||
<code style="color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px;">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="text-xs uppercase tracking-wide font-medium" style="color: var(--text-secondary)">File Path:</span>
|
||||
<code class="chip font-mono">
|
||||
@Icon("folder", "h-3.5 w-3.5")
|
||||
{ book.FilePath }
|
||||
</code>
|
||||
</div>
|
||||
if book.SHA256 != "" {
|
||||
<div>
|
||||
<span style="color: var(--text-secondary)">SHA-256:</span>
|
||||
<code style="color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px; font-size: 11px;">
|
||||
{ book.SHA256 }
|
||||
</code>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="text-xs uppercase tracking-wide font-medium" style="color: var(--text-secondary)">SHA-256:</span>
|
||||
<code class="chip font-mono text-xs" title={ book.SHA256 }>{ book.SHA256 }</code>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<span style="color: var(--text-secondary)">Last Synced:</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs uppercase tracking-wide font-medium" style="color: var(--text-secondary)">Last Synced:</span>
|
||||
<span style="color: var(--text-primary)">{ book.LastSyncTime }</span>
|
||||
</div>
|
||||
if book.ConfidenceScore > 0 {
|
||||
<div>
|
||||
<span style="color: var(--text-secondary)">Auto-Link Confidence:</span>
|
||||
<span style="color: var(--text-primary)">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs uppercase tracking-wide font-medium" style="color: var(--text-secondary)">Auto-Link Confidence:</span>
|
||||
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
{ book.ConfidenceScore }% confidence
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-4 flex gap-2 flex-wrap">
|
||||
<button
|
||||
@click="searchMatches('{ book.ProgressID }', '{ book.SHA256 }', '{ book.TitleFromDevice }')"
|
||||
class="btn-secondary px-4 py-2 rounded-lg text-sm"
|
||||
class="btn btn-secondary text-sm"
|
||||
>
|
||||
🔍 Find Potential Matches
|
||||
@Icon("search", "h-4 w-4")
|
||||
Find Potential Matches
|
||||
</button>
|
||||
<button
|
||||
@click="showManualLinkModal('{ book.ProgressID }', '{ book.TitleFromDevice }')"
|
||||
class="btn-primary px-4 py-2 rounded-lg text-sm"
|
||||
class="btn btn-primary text-sm"
|
||||
>
|
||||
🔗 Manually Link
|
||||
@Icon("edit", "h-4 w-4")
|
||||
Manually Link
|
||||
</button>
|
||||
</div>
|
||||
<div id="matches-{ book.ProgressID }" class="hidden">
|
||||
<div class="border-t pt-4" style="border-color: var(--border);">
|
||||
<div class="border-t pt-4 mt-2" style="border-color: var(--border);">
|
||||
<h4 class="font-semibold mb-3" style="color: var(--text-primary)">Potential Matches</h4>
|
||||
<div id="matches-list-{ book.ProgressID }" class="space-y-3">
|
||||
<p style="color: var(--text-secondary)">Click "Find Potential Matches" to search</p>
|
||||
@@ -124,50 +135,49 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Manual Link Modal -->
|
||||
<div id="manual-link-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
</main>
|
||||
<div id="manual-link-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md mx-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Manually Link Book</h2>
|
||||
<button @click="hideManualLinkModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<button @click="hideManualLinkModal()" class="icon-btn" aria-label="Close">@Icon("close", "h-5 w-5")</button>
|
||||
</div>
|
||||
<input type="hidden" id="link-progress-id"/>
|
||||
<input type="hidden" id="link-book-sha256"/>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Book Title from Device</label>
|
||||
<label for="link-book-title" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Book Title from Device</label>
|
||||
<input
|
||||
type="text"
|
||||
id="link-book-title"
|
||||
readonly
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-secondary); border-color: var(--border);"
|
||||
class="input"
|
||||
style="color: var(--text-secondary);"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Search for Book to Link</label>
|
||||
<label for="link-search-input" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Search for Book to Link</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
id="link-search-input"
|
||||
placeholder="Search by title, author..."
|
||||
class="flex-1 px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
placeholder="Search by title, author…"
|
||||
class="input flex-1"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="searchBooksForLink()"
|
||||
class="btn-primary px-4 py-2 rounded-lg"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
@Icon("search", "h-4 w-4")
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="link-search-results" class="mb-4 max-h-48 overflow-y-auto space-y-2">
|
||||
<p style="color: var(--text-secondary)">Search for books to link</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Search for books to link</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Confidence Score</label>
|
||||
<label for="link-confidence" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Confidence Score</label>
|
||||
<input
|
||||
type="number"
|
||||
id="link-confidence"
|
||||
@@ -175,16 +185,16 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {
|
||||
max="1"
|
||||
step="0.1"
|
||||
value="1.0"
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
class="input"
|
||||
/>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">1.0 = manual override (highest confidence)</p>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="hideManualLinkModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
<div class="flex justify-end gap-3">
|
||||
<button type="button" @click="hideManualLinkModal()" class="btn btn-secondary">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" @click="confirmManualLink()" class="btn-primary px-4 py-2 rounded-lg">
|
||||
<button type="button" @click="confirmManualLink()" class="btn btn-primary">
|
||||
@Icon("check", "h-4 w-4")
|
||||
Link Book
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -37,164 +37,261 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">Unlinked Books</h1><p style=\"color: var(--text-secondary)\">Resolve books that couldn't be automatically matched between devices</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("sync", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Unlinked Books</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Resolve books that couldn't be automatically matched between devices</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(unlinkedBooks) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"flex justify-between items-center mb-6 p-4 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex items-center gap-4\"><input type=\"checkbox\" id=\"select-all-unlinked\" @change=\"toggleAllUnlinked()\" class=\"w-5 h-5\"> <label for=\"select-all-unlinked\" class=\"cursor-pointer\" style=\"color: var(--text-primary)\">Select All</label> <span id=\"selected-count\" class=\"text-sm\" style=\"color: var(--text-secondary)\">0 selected</span></div><div class=\"flex gap-2\"><button @click=\"bulkAutoLink()\" class=\"btn-secondary px-4 py-2 rounded-lg text-sm\">🤖 Auto-Link Selected (High Confidence)</button> <button @click=\"bulkGetSuggestions()\" class=\"btn-secondary px-4 py-2 rounded-lg text-sm\">💡 Get Suggestions for Selected</button> <button @click=\"showBulkManualLink()\" class=\"btn-primary px-4 py-2 rounded-lg text-sm\">🔗 Bulk Manual Link</button></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"card p-4 mb-6 flex flex-wrap justify-between items-center gap-3\"><div class=\"flex items-center gap-4\"><input type=\"checkbox\" id=\"select-all-unlinked\" @change=\"toggleAllUnlinked()\" class=\"w-5 h-5\"> <label for=\"select-all-unlinked\" class=\"cursor-pointer text-sm font-medium\" style=\"color: var(--text-primary)\">Select All</label> <span id=\"selected-count\" class=\"text-sm\" style=\"color: var(--text-secondary)\">0 selected</span></div><div class=\"flex gap-2 flex-wrap\"><button @click=\"bulkAutoLink()\" class=\"btn btn-secondary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("sync", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Auto-Link Selected (High Confidence)</button> <button @click=\"bulkGetSuggestions()\" class=\"btn btn-secondary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("search", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Get Suggestions for Selected</button> <button @click=\"showBulkManualLink()\" class=\"btn btn-primary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("edit", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "Bulk Manual Link</button></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div id=\"unlinked-container\" class=\"space-y-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div id=\"unlinked-container\" class=\"space-y-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(unlinkedBooks) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">✓</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">All Books Linked!</h3><p>There are no unlinked books that need manual resolution.</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"card text-center py-16\"><span class=\"grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl\" style=\"background-color: color-mix(in srgb, var(--status-success) 16%, transparent); color: var(--status-success);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("check-circle", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">All Books Linked!</h3><p style=\"color: var(--text-secondary)\">There are no unlinked books that need manual resolution.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, book := range unlinkedBooks {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex gap-6\"><div class=\"flex items-start pt-2\"><input type=\"checkbox\" class=\"unlinked-checkbox w-5 h-5\" data-progress-id=\"{ book.ProgressID }\" data-title=\"{ book.TitleFromDevice }\" onchange=\"updateSelectedCount()\"></div><div class=\"flex-1\"><div class=\"flex items-center gap-3 mb-4\"><div class=\"text-4xl\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"card p-6\"><div class=\"flex gap-6\"><div class=\"flex items-start pt-2\"><input type=\"checkbox\" class=\"unlinked-checkbox w-5 h-5\" data-progress-id=\"{ book.ProgressID }\" data-title=\"{ book.TitleFromDevice }\" onchange=\"updateSelectedCount()\"></div><div class=\"flex-1 min-w-0\"><div class=\"flex items-center gap-3 mb-4\"><span class=\"grid place-items-center h-11 w-11 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if book.DeviceType == "koreader" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "📖")
|
||||
templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if book.DeviceType == "kobo" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "📚")
|
||||
templ_7745c5c3_Err = Icon("library", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "📱")
|
||||
templ_7745c5c3_Err = Icon("device", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><div><h3 class=\"font-semibold text-lg\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span><div class=\"min-w-0\"><h3 class=\"font-semibold text-lg truncate\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(book.TitleFromDevice)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 66, Col: 102}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 76, Col: 111}
|
||||
}
|
||||
_, 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, 11, "</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Device: ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Device: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 68, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 78, Col: 37}
|
||||
}
|
||||
_, 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, 12, " (")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " (")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 68, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 78, Col: 58}
|
||||
}
|
||||
_, 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, 13, ")</p></div></div><div class=\"space-y-2 text-sm mb-4\"><div><span style=\"color: var(--text-secondary)\">File Path:</span> <code style=\"color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px;\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ")</p></div></div><div class=\"space-y-2 text-sm mb-4\"><div class=\"flex items-center gap-2 flex-wrap\"><span class=\"text-xs uppercase tracking-wide font-medium\" style=\"color: var(--text-secondary)\">File Path:</span> <code class=\"chip font-mono\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("folder", "h-3.5 w-3.5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(book.FilePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 76, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 87, Col: 27}
|
||||
}
|
||||
_, 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, 14, "</code></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</code></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if book.SHA256 != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div><span style=\"color: var(--text-secondary)\">SHA-256:</span> <code style=\"color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px; font-size: 11px;\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"flex items-center gap-2 flex-wrap\"><span class=\"text-xs uppercase tracking-wide font-medium\" style=\"color: var(--text-secondary)\">SHA-256:</span> <code class=\"chip font-mono text-xs\" title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(book.SHA256)
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.SHA256)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 83, Col: 26}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 93, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</code></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(book.SHA256)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 93, Col: 84}
|
||||
}
|
||||
_, 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, 19, "</code></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div><span style=\"color: var(--text-secondary)\">Last Synced:</span> <span style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"flex items-center gap-2\"><span class=\"text-xs uppercase tracking-wide font-medium\" style=\"color: var(--text-secondary)\">Last Synced:</span> <span style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(book.LastSyncTime)
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(book.LastSyncTime)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 89, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 98, Col: 71}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
_, 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, 18, "</span></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if book.ConfidenceScore > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div><span style=\"color: var(--text-secondary)\">Auto-Link Confidence:</span> <span style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"flex items-center gap-2\"><span class=\"text-xs uppercase tracking-wide font-medium\" style=\"color: var(--text-secondary)\">Auto-Link Confidence:</span> <span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(book.ConfidenceScore)
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(book.ConfidenceScore)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 95, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 104, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
_, 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, 20, "% confidence</span></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "% confidence</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div><div class=\"mb-4\"><button @click=\"searchMatches('{ book.ProgressID }', '{ book.SHA256 }', '{ book.TitleFromDevice }')\" class=\"btn-secondary px-4 py-2 rounded-lg text-sm\">🔍 Find Potential Matches</button> <button @click=\"showManualLinkModal('{ book.ProgressID }', '{ book.TitleFromDevice }')\" class=\"btn-primary px-4 py-2 rounded-lg text-sm\">🔗 Manually Link</button></div><div id=\"matches-{ book.ProgressID }\" class=\"hidden\"><div class=\"border-t pt-4\" style=\"border-color: var(--border);\"><h4 class=\"font-semibold mb-3\" style=\"color: var(--text-primary)\">Potential Matches</h4><div id=\"matches-list-{ book.ProgressID }\" class=\"space-y-3\"><p style=\"color: var(--text-secondary)\">Click \"Find Potential Matches\" to search</p></div></div></div></div></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div><div class=\"mb-4 flex gap-2 flex-wrap\"><button @click=\"searchMatches('{ book.ProgressID }', '{ book.SHA256 }', '{ book.TitleFromDevice }')\" class=\"btn btn-secondary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("search", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Find Potential Matches</button> <button @click=\"showManualLinkModal('{ book.ProgressID }', '{ book.TitleFromDevice }')\" class=\"btn btn-primary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("edit", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Manually Link</button></div><div id=\"matches-{ book.ProgressID }\" class=\"hidden\"><div class=\"border-t pt-4 mt-2\" style=\"border-color: var(--border);\"><h4 class=\"font-semibold mb-3\" style=\"color: var(--text-primary)\">Potential Matches</h4><div id=\"matches-list-{ book.ProgressID }\" class=\"space-y-3\"><p style=\"color: var(--text-secondary)\">Click \"Find Potential Matches\" to search</p></div></div></div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div><!-- Manual Link Modal --><div id=\"manual-link-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Manually Link Book</h2><button @click=\"hideManualLinkModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><input type=\"hidden\" id=\"link-progress-id\"> <input type=\"hidden\" id=\"link-book-sha256\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Book Title from Device</label> <input type=\"text\" id=\"link-book-title\" readonly class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-secondary); border-color: var(--border);\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Search for Book to Link</label><div class=\"flex gap-2\"><input type=\"text\" id=\"link-search-input\" placeholder=\"Search by title, author...\" class=\"flex-1 px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"> <button type=\"button\" @click=\"searchBooksForLink()\" class=\"btn-primary px-4 py-2 rounded-lg\">Search</button></div></div><div id=\"link-search-results\" class=\"mb-4 max-h-48 overflow-y-auto space-y-2\"><p style=\"color: var(--text-secondary)\">Search for books to link</p></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Confidence Score</label> <input type=\"number\" id=\"link-confidence\" min=\"0\" max=\"1\" step=\"0.1\" value=\"1.0\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">1.0 = manual override (highest confidence)</p></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"hideManualLinkModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"button\" @click=\"confirmManualLink()\" class=\"btn-primary px-4 py-2 rounded-lg\">Link Book</button></div></div></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div></main><div id=\"manual-link-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md mx-4\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Manually Link Book</h2><button @click=\"hideManualLinkModal()\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</button></div><input type=\"hidden\" id=\"link-progress-id\"> <input type=\"hidden\" id=\"link-book-sha256\"><div class=\"mb-4\"><label for=\"link-book-title\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Book Title from Device</label> <input type=\"text\" id=\"link-book-title\" readonly class=\"input\" style=\"color: var(--text-secondary);\"></div><div class=\"mb-4\"><label for=\"link-search-input\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Search for Book to Link</label><div class=\"flex gap-2\"><input type=\"text\" id=\"link-search-input\" placeholder=\"Search by title, author…\" class=\"input flex-1\"> <button type=\"button\" @click=\"searchBooksForLink()\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("search", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "Search</button></div></div><div id=\"link-search-results\" class=\"mb-4 max-h-48 overflow-y-auto space-y-2\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Search for books to link</p></div><div class=\"mb-4\"><label for=\"link-confidence\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Confidence Score</label> <input type=\"number\" id=\"link-confidence\" min=\"0\" max=\"1\" step=\"0.1\" value=\"1.0\" class=\"input\"><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">1.0 = manual override (highest confidence)</p></div><div class=\"flex justify-end gap-3\"><button type=\"button\" @click=\"hideManualLinkModal()\" class=\"btn btn-secondary\">Cancel</button> <button type=\"button\" @click=\"confirmManualLink()\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("check", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Link Book</button></div></div></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+14
-5
@@ -15,11 +15,20 @@ import (
|
||||
)
|
||||
|
||||
func activeClass(current, target string) string {
|
||||
base := "block px-4 py-2 rounded-lg "
|
||||
base := "flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors "
|
||||
if current == target {
|
||||
return base + "bg-accent text-bg-primary"
|
||||
return base + "bg-brand/15 text-brand"
|
||||
}
|
||||
return base + "hover:opacity-80"
|
||||
return base + "text-content-muted hover:bg-surface-hover hover:text-content"
|
||||
}
|
||||
|
||||
// navItemClass returns classes for top-nav links, marking the active route.
|
||||
func navItemClass(current, target string) string {
|
||||
base := "text-sm font-medium px-3 py-2 rounded-lg transition-colors "
|
||||
if current == target {
|
||||
return base + "text-content bg-surface-hover"
|
||||
}
|
||||
return base + "text-content-muted hover:text-content hover:bg-surface-hover"
|
||||
}
|
||||
|
||||
func ContainsString(slice []string, item string) bool {
|
||||
@@ -66,13 +75,13 @@ func renderStars(rating int32) string {
|
||||
for i := int32(0); i < totalStars; i++ {
|
||||
if i < fullStars {
|
||||
// Filled star - accent color (theme-aware highlight)
|
||||
stars.WriteString(`<span style="color: var(--accent);">★</span>`)
|
||||
stars.WriteString(`<span class="text-brand">★</span>`)
|
||||
} else if i == fullStars && hasHalf {
|
||||
// Half star - gradient from accent (left) to grey (right)
|
||||
stars.WriteString(`<span style="background: linear-gradient(90deg, var(--accent) 50%, var(--text-secondary) 50%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">★</span>`)
|
||||
} else {
|
||||
// Empty star - grey using theme variable
|
||||
stars.WriteString(`<span style="color: var(--text-secondary);">★</span>`)
|
||||
stars.WriteString(`<span class="text-content-muted/40">★</span>`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-10
@@ -48,18 +48,16 @@ Alpine.store("bookPicker", {
|
||||
},
|
||||
|
||||
clearFilters() {
|
||||
const filterForm = document.getElementById(
|
||||
"book-picker-filters",
|
||||
) as HTMLFormElement;
|
||||
if (!filterForm) return;
|
||||
const filterDiv = document.getElementById("book-picker-filters");
|
||||
if (!filterDiv) return;
|
||||
|
||||
// Reset all form fields except checkbox state (that's in Alpine.store)
|
||||
const inputs = filterForm.querySelectorAll("input:not([type='checkbox'])");
|
||||
const inputs = filterDiv.querySelectorAll(
|
||||
'input[type="text"]',
|
||||
) as NodeListOf<HTMLInputElement>;
|
||||
inputs.forEach((input) => {
|
||||
(input as HTMLInputElement).value = "";
|
||||
input.value = "";
|
||||
});
|
||||
|
||||
// Reload books - selection state preserved in Alpine.store
|
||||
this.loadBooks();
|
||||
},
|
||||
|
||||
@@ -97,8 +95,7 @@ Alpine.store("bookPicker", {
|
||||
"success",
|
||||
);
|
||||
this.close();
|
||||
// Reload collection detail page
|
||||
window.htmx.trigger(document.body, "reloadCollection");
|
||||
location.reload();
|
||||
} else {
|
||||
showToast("Failed to add books", "error");
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ Alpine.data("bookshelf", () => ({
|
||||
// Component state
|
||||
showSaveModal: false,
|
||||
showFiltersDropdown: false,
|
||||
filtersOpen: false,
|
||||
hasCoverState: null as boolean | null,
|
||||
dropdownAlign: "right" as "left" | "right",
|
||||
resizeTimeout: null as number | null,
|
||||
|
||||
+152
-21
@@ -127,6 +127,9 @@ function renderCollectionBooks(books: BookInfo[]): void {
|
||||
const container = document.getElementById("books-container");
|
||||
if (!container) return;
|
||||
|
||||
const dataEl = document.getElementById("collection-data");
|
||||
const isSystem = dataEl?.dataset.isSystem === "true";
|
||||
|
||||
if (books.length === 0) {
|
||||
container.innerHTML = `<div id="empty-state" class="col-span-full text-center py-16" style="color: var(--text-secondary)">No books in this collection yet.</div>`;
|
||||
return;
|
||||
@@ -135,29 +138,37 @@ function renderCollectionBooks(books: BookInfo[]): void {
|
||||
container.innerHTML = books
|
||||
.map(
|
||||
(book) => `
|
||||
<a href="/media/${book.media_item_id}">
|
||||
<div class="card p-4 rounded-lg border hover:shadow-lg transition-shadow"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0 pt-1">
|
||||
<input type="checkbox" onchange="toggleBookForRemoval('${book.media_item_id}')" class="w-5 h-5"/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-lg mb-1 line-clamp-2" style="color: var(--text-primary)">${book.title}</h3>
|
||||
${book.author ? `<p class="text-sm line-clamp-1" style="color: var(--text-secondary)">by ${book.author}</p>` : ""}
|
||||
</div>
|
||||
<div class="flex-shrink-0 w-16 sm:w-20">
|
||||
<div class="card p-4 rounded-2xl collection-book-card" data-title="${book.title}" data-author="${book.author || ""}" data-media-id="${book.media_item_id}">
|
||||
<div class="flex gap-4">
|
||||
${isSystem ? "" : `
|
||||
<div class="flex-shrink-0 pt-1">
|
||||
<label class="flex items-center cursor-pointer p-2 -m-2">
|
||||
<input type="checkbox" class="w-5 h-5"
|
||||
:checked="selectedBooks.includes('${book.media_item_id}')"
|
||||
@change="toggleSelection('${book.media_item_id}')"/>
|
||||
</label>
|
||||
</div>`}
|
||||
<div class="flex-1 min-w-0">
|
||||
<a href="/media/${book.media_item_id}">
|
||||
<h3 class="font-semibold text-lg mb-1 line-clamp-2 hover:underline" style="color: var(--text-primary)">${book.title}</h3>
|
||||
</a>
|
||||
${book.author ? `<p class="text-sm line-clamp-1" style="color: var(--text-secondary)">by ${book.author}</p>` : ""}
|
||||
</div>
|
||||
<div class="flex-shrink-0 w-16 sm:w-20">
|
||||
<a href="/media/${book.media_item_id}">
|
||||
<img src="${book.cover_image_path || "/static/placeholder-book.svg"}" alt="Cover"
|
||||
class="w-full aspect-[3/4] object-cover rounded shadow-md"
|
||||
onerror="this.src='/static/placeholder-book.svg'"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
|
||||
<button @click="removeBook('${book.media_item_id}')" class="px-3 py-1 text-sm border rounded hover:opacity-80"
|
||||
style="border-color: var(--border); color: var(--text-secondary);">🗑️ Remove from Collection</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
${isSystem ? "" : `
|
||||
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
|
||||
<button @click="requestRemoveBook('${book.media_item_id}')" class="btn btn-secondary w-full">
|
||||
🗑️ Remove from Collection
|
||||
</button>
|
||||
</div>`}
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
@@ -352,7 +363,7 @@ function selectColor(color: string): void {
|
||||
(btn as HTMLElement).style.outlineOffset = "0";
|
||||
});
|
||||
const selectedBtn = document.querySelector(
|
||||
`.color-option[onclick="selectColor('${color}')"]`,
|
||||
`.color-option[data-color="${color}"]`,
|
||||
) as HTMLElement;
|
||||
if (selectedBtn) {
|
||||
selectedBtn.style.outline = "3px solid var(--text-primary)";
|
||||
@@ -391,6 +402,10 @@ function setupHTMXModalInit(): void {
|
||||
const target = evt.detail.target;
|
||||
if (target && target.id === "modal-container") {
|
||||
populateIconGrid();
|
||||
initColorSelection();
|
||||
Alpine.initTree(target);
|
||||
}
|
||||
if (target && target.id === "book-picker-grid") {
|
||||
Alpine.initTree(target);
|
||||
}
|
||||
});
|
||||
@@ -459,8 +474,9 @@ function populateIconGrid(): void {
|
||||
button.type = "button";
|
||||
button.className = "icon-btn text-2xl p-1 hover:bg-opacity-80 rounded";
|
||||
button.textContent = emoji;
|
||||
button.setAttribute("onclick", `selectIcon('${emoji}')`);
|
||||
button.setAttribute("data-emoji", emoji);
|
||||
button.setAttribute("data-keywords", keywords.join(","));
|
||||
button.addEventListener("click", () => selectIcon(emoji));
|
||||
iconGrid.appendChild(button);
|
||||
});
|
||||
}
|
||||
@@ -482,7 +498,7 @@ function selectIcon(icon: string): void {
|
||||
});
|
||||
|
||||
const selectedBtn = document.querySelector(
|
||||
`.icon-btn[onclick="selectIcon('${icon}')"]`,
|
||||
`.icon-btn[data-emoji="${icon}"]`,
|
||||
) as HTMLElement;
|
||||
if (selectedBtn) {
|
||||
selectedBtn.style.outline = "2px solid var(--text-primary)";
|
||||
@@ -553,10 +569,90 @@ function initCollectionsPage(): void {
|
||||
setupHTMXModalInit();
|
||||
}
|
||||
|
||||
function getCollectionId(): string {
|
||||
const dataEl = document.getElementById("collection-data");
|
||||
return dataEl?.dataset.id || "";
|
||||
}
|
||||
|
||||
function toggleSelection(this: any, id: string): void {
|
||||
const idx = this.selectedBooks.indexOf(id);
|
||||
if (idx >= 0) {
|
||||
this.selectedBooks.splice(idx, 1);
|
||||
} else {
|
||||
this.selectedBooks.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
async function doRemoveBook(id: string): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/collections/${getCollectionId()}/books/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
},
|
||||
);
|
||||
if (response.ok) {
|
||||
showToast("Removed from collection", "success");
|
||||
setTimeout(() => location.reload(), 500);
|
||||
} else {
|
||||
showToast("Failed to remove book", "error");
|
||||
}
|
||||
} catch {
|
||||
showToast("Error removing book", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function doBulkRemove(ids: string[]): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/collections/${getCollectionId()}/books/bulk-remove`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ book_ids: ids }),
|
||||
},
|
||||
);
|
||||
if (response.ok) {
|
||||
showToast(`Removed ${ids.length} book(s)`, "success");
|
||||
setTimeout(() => location.reload(), 500);
|
||||
} else {
|
||||
showToast("Failed to remove books", "error");
|
||||
}
|
||||
} catch {
|
||||
showToast("Error removing books", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function filterCollectionBooks(): void {
|
||||
const input = document.getElementById(
|
||||
"collection-search",
|
||||
) as HTMLInputElement;
|
||||
if (!input) return;
|
||||
const query = input.value.toLowerCase();
|
||||
const cards = document.querySelectorAll<HTMLElement>(
|
||||
".collection-book-card",
|
||||
);
|
||||
cards.forEach((card) => {
|
||||
const title = (card.dataset.title || "").toLowerCase();
|
||||
const author = (card.dataset.author || "").toLowerCase();
|
||||
card.style.display =
|
||||
title.includes(query) || author.includes(query) ? "" : "none";
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
closeCollectionModal,
|
||||
createRule,
|
||||
deleteRule,
|
||||
filterCollectionBooks,
|
||||
filterIcons,
|
||||
initColorSelection,
|
||||
initializeCollectionWebSocket,
|
||||
@@ -571,12 +667,14 @@ export {
|
||||
setupHTMXModalInit,
|
||||
showAllIcons,
|
||||
testRule,
|
||||
toggleSelection,
|
||||
};
|
||||
|
||||
Alpine.data("collections", () => ({
|
||||
closeCollectionModal,
|
||||
createRule,
|
||||
deleteRule,
|
||||
filterCollectionBooks,
|
||||
filterIcons,
|
||||
initColorSelection,
|
||||
initializeCollectionWebSocket,
|
||||
@@ -587,7 +685,40 @@ Alpine.data("collections", () => ({
|
||||
populateIconGrid,
|
||||
selectColor,
|
||||
selectIcon,
|
||||
selectedBooks: [] as string[],
|
||||
setupHTMXModalInit,
|
||||
showAllIcons,
|
||||
testRule,
|
||||
toggleSelection,
|
||||
|
||||
showConfirm: false,
|
||||
confirmMessage: "",
|
||||
confirmLabel: "Remove",
|
||||
pendingAction: null as null | (() => void),
|
||||
|
||||
requestRemoveBook(this: any, id: string) {
|
||||
this.confirmMessage = "Remove this book from the collection?";
|
||||
this.pendingAction = () => doRemoveBook(id);
|
||||
this.showConfirm = true;
|
||||
},
|
||||
|
||||
requestBulkRemove(this: any) {
|
||||
if (this.selectedBooks.length === 0) return;
|
||||
this.confirmMessage = `Remove ${this.selectedBooks.length} book(s) from this collection?`;
|
||||
const ids = [...this.selectedBooks];
|
||||
this.pendingAction = () => doBulkRemove(ids);
|
||||
this.showConfirm = true;
|
||||
},
|
||||
|
||||
executeConfirmed(this: any) {
|
||||
this.showConfirm = false;
|
||||
const action = this.pendingAction;
|
||||
this.pendingAction = null;
|
||||
if (action) action();
|
||||
},
|
||||
|
||||
closeConfirm(this: any) {
|
||||
this.showConfirm = false;
|
||||
this.pendingAction = null;
|
||||
},
|
||||
}));
|
||||
|
||||
+43
-23
@@ -205,7 +205,8 @@ function renderSectionHTML(section: SectionData): string {
|
||||
<div class="carousel-container relative group">
|
||||
<button class="carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10
|
||||
w-12 h-full bg-gradient-to-r from-gray-900 to-transparent
|
||||
flex items-center justify-start opacity-0 group-hover:opacity-100
|
||||
flex items-center justify-start opacity-0 pointer-events-none
|
||||
group-hover:pointer-events-auto group-hover:opacity-100
|
||||
transition-opacity duration-200"
|
||||
data-action="scroll-carousel"
|
||||
data-collection-id="${section.id}"
|
||||
@@ -230,7 +231,8 @@ function renderSectionHTML(section: SectionData): string {
|
||||
|
||||
<button class="carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10
|
||||
w-12 h-full bg-gradient-to-l from-gray-900 to-transparent
|
||||
flex items-center justify-end opacity-0 group-hover:opacity-100
|
||||
flex items-center justify-end opacity-0 pointer-events-none
|
||||
group-hover:pointer-events-auto group-hover:opacity-100
|
||||
transition-opacity duration-200"
|
||||
data-action="scroll-carousel"
|
||||
data-collection-id="${section.id}"
|
||||
@@ -268,33 +270,51 @@ function renderDashboardCollections(sections: SectionData[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
const BOOK_OPEN_ICON =
|
||||
'<svg class="reader-icon h-5 w-5" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
||||
'<path d="M12 7v14"></path>' +
|
||||
'<path d="M3 4h6a3 3 0 0 1 3 3v14a2 2 0 0 0-2-2H3z"></path>' +
|
||||
'<path d="M21 4h-6a3 3 0 0 0-3 3v14a2 2 0 0 1 2-2h7z"></path>' +
|
||||
"</svg>";
|
||||
|
||||
function renderBookCard(book: BookInfo): string {
|
||||
const coverUrl = book.cover_image_path || "/static/placeholder-book.svg";
|
||||
const actionHref = book.has_conflict
|
||||
? `/media/${book.media_item_id}`
|
||||
: `/readers/${book.media_item_id}`;
|
||||
const actionLabel = book.has_conflict
|
||||
? `Resolve progress conflict for ${book.title}`
|
||||
: `Read ${book.title}`;
|
||||
const actionTitle = book.has_conflict
|
||||
? "Resolve progress conflict"
|
||||
: "Read";
|
||||
|
||||
return `
|
||||
<a href="/media/${book.media_item_id}">
|
||||
<div class="book-card flex-shrink-0 w-36 rounded-lg overflow-hidden snap-start cursor-pointer
|
||||
transition-transform duration-200 hover:scale-105"
|
||||
data-media-item-id="${book.media_item_id}"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="View ${book.title}">
|
||||
<div class="aspect-[2/3] overflow-hidden shadow-lg
|
||||
bg-gradient-to-br from-gray-700 to-gray-900">
|
||||
<img src="${coverUrl}"
|
||||
alt="${book.title}"
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
onerror="this.src='/static/placeholder-book.svg'">
|
||||
</div>
|
||||
<div class="book-card-text px-2 py-1 bg-[color-mix(in_srgb,var(--wood-border)_40%,transparent)]">
|
||||
<h3 class="font-semibold text-base line-clamp-2" style="color: var(--text-primary)">
|
||||
${book.title}
|
||||
</h3>
|
||||
${book.author ? `<p class="text-sm line-clamp-1" style="color: var(--text-secondary)">${book.author}</p>` : ""}
|
||||
<div class="flex-shrink-0 w-36 sm:w-40 snap-start">
|
||||
<div class="book-card relative w-full h-full rounded-xl overflow-hidden cursor-pointer">
|
||||
<a href="/media/${book.media_item_id}" class="block h-full">
|
||||
<div class="book-card-cover aspect-[2/3] overflow-hidden" style="background-color: color-mix(in srgb, var(--text-primary) 8%, transparent);">
|
||||
<img src="${coverUrl}"
|
||||
alt="${book.title}"
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
onerror="this.src='/static/placeholder-book.svg'">
|
||||
</div>
|
||||
<div class="book-card-meta">
|
||||
<h3 class="font-semibold text-sm leading-snug line-clamp-2" style="color: var(--text-primary)" title="${book.title}">
|
||||
${book.title}
|
||||
</h3>
|
||||
${book.author ? `<p class="text-xs mt-0.5 line-clamp-1" style="color: var(--text-secondary)" title="${book.author}">${book.author}</p>` : ""}
|
||||
</div>
|
||||
</a>
|
||||
<div class="book-card-action">
|
||||
<a href="${actionHref}"
|
||||
class="book-card-action-btn"
|
||||
aria-label="${actionLabel}"
|
||||
title="${actionTitle}">${BOOK_OPEN_ICON}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
+121
-51
@@ -372,6 +372,8 @@ document.addEventListener("alpine:init", () => {
|
||||
interactionMode: "select" as string,
|
||||
magnifierEnabled: false,
|
||||
progressText: "",
|
||||
progressLabel: "",
|
||||
progressMain: "",
|
||||
sliderValue: 0,
|
||||
settings: null as ReaderSettings | null,
|
||||
justify: true,
|
||||
@@ -405,6 +407,34 @@ document.addEventListener("alpine:init", () => {
|
||||
label: string;
|
||||
startPage: number;
|
||||
}[],
|
||||
init() {
|
||||
const link = document.getElementById("reader-back");
|
||||
if (!link) return;
|
||||
const storageKey = "reader_back";
|
||||
if (document.referrer) {
|
||||
try {
|
||||
const ref = new URL(document.referrer);
|
||||
if (
|
||||
ref.origin === window.location.origin &&
|
||||
!ref.pathname.startsWith("/readers/") &&
|
||||
ref.pathname !== window.location.pathname
|
||||
) {
|
||||
sessionStorage.setItem(storageKey, ref.pathname + ref.search);
|
||||
}
|
||||
} catch {}
|
||||
} else {
|
||||
sessionStorage.removeItem(storageKey);
|
||||
}
|
||||
const backUrl =
|
||||
sessionStorage.getItem(storageKey) ||
|
||||
link.getAttribute("href") ||
|
||||
"/dashboard";
|
||||
link.addEventListener("click", (e: Event) => {
|
||||
e.preventDefault();
|
||||
sessionStorage.removeItem(storageKey);
|
||||
window.location.href = backUrl;
|
||||
});
|
||||
},
|
||||
async initReader(config: {
|
||||
mediaItemId: string;
|
||||
fileUrl: string;
|
||||
@@ -489,21 +519,20 @@ document.addEventListener("alpine:init", () => {
|
||||
tocItem,
|
||||
section,
|
||||
};
|
||||
const progressStr = this.formatProgress(
|
||||
const progressParts = this.formatProgressParts(
|
||||
fraction,
|
||||
location,
|
||||
pageItem,
|
||||
tocItem,
|
||||
section,
|
||||
);
|
||||
this.progressText = progressStr;
|
||||
this.setProgress(progressParts);
|
||||
this.sliderValue = fraction;
|
||||
const slider = document.getElementById(
|
||||
"progress-slider",
|
||||
) as HTMLInputElement;
|
||||
if (slider) {
|
||||
slider.value = fraction;
|
||||
slider.title = progressStr;
|
||||
}
|
||||
const range = e.detail.range as Range | undefined;
|
||||
if (range) {
|
||||
@@ -547,6 +576,34 @@ document.addEventListener("alpine:init", () => {
|
||||
}
|
||||
this.initTime = Date.now();
|
||||
this.fetchReadingSpeed();
|
||||
this.setupViewportInsets();
|
||||
},
|
||||
updateViewportInsets() {
|
||||
const top = document.getElementById("reader-topbar");
|
||||
const bot = document.getElementById("reader-bottombar");
|
||||
const vp = document.getElementById("reader-viewport");
|
||||
if (!top || !bot || !vp) return;
|
||||
const margin = 6;
|
||||
vp.style.setProperty("top", `${top.offsetHeight + margin}px`);
|
||||
vp.style.setProperty("bottom", `${bot.offsetHeight + margin}px`);
|
||||
},
|
||||
setupViewportInsets() {
|
||||
const top = document.getElementById("reader-topbar");
|
||||
const bot = document.getElementById("reader-bottombar");
|
||||
this.updateViewportInsets();
|
||||
window.addEventListener("resize", () => this.updateViewportInsets());
|
||||
window.addEventListener("orientationchange", () =>
|
||||
setTimeout(() => this.updateViewportInsets(), 250),
|
||||
);
|
||||
if (
|
||||
typeof ResizeObserver !== "undefined" &&
|
||||
top &&
|
||||
bot
|
||||
) {
|
||||
const ro = new ResizeObserver(() => this.updateViewportInsets());
|
||||
ro.observe(top);
|
||||
ro.observe(bot);
|
||||
}
|
||||
},
|
||||
debouncedSaveProgress(fraction: number, location: any, cfi: string) {
|
||||
if (Date.now() - this.initTime < 5000) return;
|
||||
@@ -774,19 +831,20 @@ document.addEventListener("alpine:init", () => {
|
||||
/* ignore bookmark errors for now */
|
||||
}
|
||||
},
|
||||
formatProgress(
|
||||
formatProgressParts(
|
||||
fraction: number,
|
||||
location: { current: number; next: number; total: number },
|
||||
pageItem: { id: number; label: string; href: string },
|
||||
tocItem: FoliateTocItem | null,
|
||||
section: { current: number; total: number },
|
||||
): string {
|
||||
): { label: string; main: string } {
|
||||
const percent = new Intl.NumberFormat("en", { style: "percent" }).format(
|
||||
fraction,
|
||||
);
|
||||
const chapterLabel = tocItem?.label ? `${tocItem.label} · ` : "";
|
||||
switch (this.progressMode) {
|
||||
case "percentage":
|
||||
return percent;
|
||||
return { label: "", main: percent };
|
||||
case "chapter": {
|
||||
if (this.isFixedLayout && tocItem && this.chapterBoundaries.length > 0) {
|
||||
const totalSections = this.book?.sections?.filter(
|
||||
@@ -809,10 +867,10 @@ document.addEventListener("alpine:init", () => {
|
||||
1,
|
||||
Math.min(currentPage - chapterStart + 1, chapterPages),
|
||||
);
|
||||
const label = tocItem.label
|
||||
? `${tocItem.label} · `
|
||||
: "";
|
||||
return `${label}${currentInChapter} / ${chapterPages}`;
|
||||
return {
|
||||
label: chapterLabel,
|
||||
main: `${currentInChapter} / ${chapterPages}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (tocItem && this.chapterBoundaries.length > 0) {
|
||||
@@ -836,10 +894,10 @@ document.addEventListener("alpine:init", () => {
|
||||
1,
|
||||
Math.min(currentPage - chapterStart + 1, chapterPages),
|
||||
);
|
||||
const label = tocItem.label
|
||||
? `${tocItem.label} · `
|
||||
: "";
|
||||
return `${label}${currentInChapter} / ${chapterPages}`;
|
||||
return {
|
||||
label: chapterLabel,
|
||||
main: `${currentInChapter} / ${chapterPages}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (section && this.sectionFractionsArr.length > 1) {
|
||||
@@ -859,16 +917,19 @@ document.addEventListener("alpine:init", () => {
|
||||
Math.round((fraction - startFrac) * pageBase),
|
||||
);
|
||||
const clamped = Math.min(currentInSec, totalInSec);
|
||||
const label = tocItem?.label
|
||||
? `${tocItem.label} · `
|
||||
: "";
|
||||
return `${label}${clamped} / ${totalInSec}`;
|
||||
return {
|
||||
label: chapterLabel,
|
||||
main: `${clamped} / ${totalInSec}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (location.total > 0) {
|
||||
return `${percent} · ${location.current} / ${location.total}`;
|
||||
return {
|
||||
label: "",
|
||||
main: `${percent} · ${location.current} / ${location.total}`,
|
||||
};
|
||||
}
|
||||
return percent;
|
||||
return { label: "", main: percent };
|
||||
}
|
||||
case "time-left": {
|
||||
if (this.readingSpeedPpm > 0 && location.total > 0) {
|
||||
@@ -877,30 +938,47 @@ document.addEventListener("alpine:init", () => {
|
||||
if (mins >= 60) {
|
||||
const hrs = Math.floor(mins / 60);
|
||||
const m = mins % 60;
|
||||
return `${percent} · ~${hrs}h ${m}m left`;
|
||||
return { label: "", main: `${percent} · ~${hrs}h ${m}m left` };
|
||||
}
|
||||
return `${percent} · ~${mins} min left`;
|
||||
return { label: "", main: `${percent} · ~${mins} min left` };
|
||||
}
|
||||
return `${percent} · ~-- min left`;
|
||||
return { label: "", main: `${percent} · ~-- min left` };
|
||||
}
|
||||
default: {
|
||||
if (this.isFixedLayout) {
|
||||
const pageInfo = this.getRenderedPageInfo();
|
||||
if (pageInfo) {
|
||||
return `${pageInfo.current} / ${pageInfo.total}`;
|
||||
return { label: "", main: `${pageInfo.current} / ${pageInfo.total}` };
|
||||
}
|
||||
}
|
||||
if (pageItem) {
|
||||
return `${percent} · Page ${pageItem.label}`;
|
||||
return { label: "", main: `${percent} · Page ${pageItem.label}` };
|
||||
}
|
||||
const pageInfoReflow = this.getRenderedPageInfo();
|
||||
if (pageInfoReflow) {
|
||||
return `${percent} · ${pageInfoReflow.current + 1} / ${pageInfoReflow.total}`;
|
||||
return {
|
||||
label: "",
|
||||
main: `${percent} · ${pageInfoReflow.current + 1} / ${pageInfoReflow.total}`,
|
||||
};
|
||||
}
|
||||
return `${percent} · ${location.current} / ${location.total}`;
|
||||
return {
|
||||
label: "",
|
||||
main: `${percent} · ${location.current} / ${location.total}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
setProgress(parts: { label: string; main: string }, sliderTitle?: string) {
|
||||
this.progressLabel = parts.label;
|
||||
this.progressMain = parts.main;
|
||||
this.progressText = parts.label + parts.main;
|
||||
const slider = document.getElementById(
|
||||
"progress-slider",
|
||||
) as HTMLInputElement;
|
||||
if (slider) {
|
||||
slider.title = sliderTitle ?? this.progressText;
|
||||
}
|
||||
},
|
||||
cycleProgressMode() {
|
||||
const modes = ["pages", "chapter", "percentage", "time-left"];
|
||||
const idx = modes.indexOf(this.progressMode);
|
||||
@@ -909,19 +987,15 @@ document.addEventListener("alpine:init", () => {
|
||||
if (this.lastRelocateDetail) {
|
||||
const { fraction, location, pageItem, tocItem, section } =
|
||||
this.lastRelocateDetail;
|
||||
this.progressText = this.formatProgress(
|
||||
fraction,
|
||||
location,
|
||||
pageItem,
|
||||
tocItem,
|
||||
section,
|
||||
this.setProgress(
|
||||
this.formatProgressParts(
|
||||
fraction,
|
||||
location,
|
||||
pageItem,
|
||||
tocItem,
|
||||
section,
|
||||
),
|
||||
);
|
||||
const slider = document.getElementById(
|
||||
"progress-slider",
|
||||
) as HTMLInputElement;
|
||||
if (slider) {
|
||||
slider.title = this.progressText;
|
||||
}
|
||||
}
|
||||
},
|
||||
async fetchReadingSpeed() {
|
||||
@@ -944,19 +1018,15 @@ document.addEventListener("alpine:init", () => {
|
||||
if (this.lastRelocateDetail) {
|
||||
const { fraction, location, pageItem, tocItem, section } =
|
||||
this.lastRelocateDetail;
|
||||
this.progressText = this.formatProgress(
|
||||
fraction,
|
||||
location,
|
||||
pageItem,
|
||||
tocItem,
|
||||
section,
|
||||
this.setProgress(
|
||||
this.formatProgressParts(
|
||||
fraction,
|
||||
location,
|
||||
pageItem,
|
||||
tocItem,
|
||||
section,
|
||||
),
|
||||
);
|
||||
const slider = document.getElementById(
|
||||
"progress-slider",
|
||||
) as HTMLInputElement;
|
||||
if (slider) {
|
||||
slider.title = this.progressText;
|
||||
}
|
||||
}
|
||||
},
|
||||
computeFixedLayoutChapterBoundaries() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user