feat: Add collection detail page with /collections/:id route
Add comprehensive collection detail page that works for both system collections
(continue-reading, recently-added, not-started) and user collections.
Backend changes:
- Add new /collections/:id route in internal/router/frontend.go
- Fetches collection using GetCollection with UUID parameter
- Determines collection type from QueryType field
- Resolves library_id for system collections
- Converts database.MediaItems to handlers.BookInfo for display
- Renders CollectionDetail template with collection and books data
- Update SectionData struct in internal/handlers/collections.go
- Add CollectionID string field for view all links
- Update BuildSections() in internal/handlers/dashboard.go
- Pass CollectionID to SectionData for proper link generation
- Simplify getViewAllURL() in internal/handlers/dashboard.go
- Return /collections/{collectionID} instead of /section/{type}
- Works uniformly for both system and user collections
Frontend changes:
- Fix CollectionDetail template in templates/collections.templ
- Fix broken div nesting causing compilation error
- Add null check for CoverImagePath to prevent broken images
- Update aspect ratio to modern aspect-[3/4] syntax
- Use responsive widths (w-16 sm:w-20) for mobile/desktop
- Improve card layout with horizontal flex structure
- Add placeholder image fallback for books without covers
- Remove erroneous renderBooks() function call
This change aligns with the backend update where system collections are
now pre-made user collections in the database with query_type fields.
All collections can now use the same CollectionDetail template for a
consistent viewing experience.
This commit is contained in:
@@ -75,14 +75,15 @@ type BookInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SectionData struct {
|
type SectionData struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
IsSystem bool `json:"is_system"`
|
CollectionID string `json:"collection_id"`
|
||||||
Title string `json:"title"`
|
IsSystem bool `json:"is_system"`
|
||||||
Description string `json:"description"`
|
Title string `json:"title"`
|
||||||
Icon string `json:"icon"`
|
Description string `json:"description"`
|
||||||
Items []BookInfo `json:"items"`
|
Icon string `json:"icon"`
|
||||||
ViewAllURL string `json:"view_all_url"`
|
Items []BookInfo `json:"items"`
|
||||||
Priority int `json:"priority"`
|
ViewAllURL string `json:"view_all_url"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *CollectionHandler) CreateCollection(c echo.Context) error {
|
func (h *CollectionHandler) CreateCollection(c echo.Context) error {
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ func (h *DashboardHandler) GetSections(c echo.Context) error {
|
|||||||
prefs, _ := h.dashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
prefs, _ := h.dashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
||||||
|
|
||||||
limit := 20
|
limit := 20
|
||||||
|
if prefs.ItemsPerSection.Valid {
|
||||||
|
limit = int(prefs.ItemsPerSection.Int32)
|
||||||
|
}
|
||||||
if limitStr := c.QueryParam("limit"); limitStr != "" {
|
if limitStr := c.QueryParam("limit"); limitStr != "" {
|
||||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
||||||
limit = l
|
limit = l
|
||||||
@@ -83,6 +86,11 @@ func (h *DashboardHandler) UpdatePreferences(c echo.Context) error {
|
|||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitize preferences (remove duplicates)
|
||||||
|
cleanHidden, cleanOrder := h.dashboardService.SanitizeDashboardPreferences(req.HiddenCollections, req.CollectionOrder)
|
||||||
|
req.HiddenCollections = cleanHidden
|
||||||
|
req.CollectionOrder = cleanOrder
|
||||||
|
|
||||||
prefs, err := h.dashboardService.UpsertDashboardPreferences(c.Request().Context(), database.UpsertDashboardPreferencesParams{
|
prefs, err := h.dashboardService.UpsertDashboardPreferences(c.Request().Context(), database.UpsertDashboardPreferencesParams{
|
||||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
||||||
@@ -157,29 +165,49 @@ func BuildSections(sections []services.DashboardSection) []SectionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, SectionData{
|
result = append(result, SectionData{
|
||||||
ID: ds.CollectionName,
|
ID: ds.CollectionName,
|
||||||
IsSystem: ds.IsSystem,
|
CollectionID: ds.CollectionID.String(),
|
||||||
Title: ds.Title,
|
IsSystem: ds.IsSystem,
|
||||||
Description: ds.Description,
|
Title: ds.Title,
|
||||||
Icon: ds.Icon,
|
Description: ds.Description,
|
||||||
Items: bookCards,
|
Icon: ds.Icon,
|
||||||
ViewAllURL: getViewAllURL(ds.CollectionName, ds.QueryType),
|
Items: bookCards,
|
||||||
Priority: ds.Priority,
|
ViewAllURL: getViewAllURL(ds.CollectionID.String()),
|
||||||
|
Priority: ds.Priority,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func getViewAllURL(key, queryType string) string {
|
func getViewAllURL(collectionID string) string {
|
||||||
urls := map[string]string{
|
if collectionID != "" {
|
||||||
"continue-reading": "/section/continue-reading",
|
return "/collections/" + collectionID
|
||||||
"recently-added": "/section/recently-added",
|
|
||||||
"recently-read": "/history",
|
|
||||||
"not-started": "/section/not-started",
|
|
||||||
}
|
|
||||||
if url, exists := urls[queryType]; exists {
|
|
||||||
return url
|
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *DashboardHandler) GetPreferences(c echo.Context) error {
|
||||||
|
user := c.Get("user").(database.Users)
|
||||||
|
userUUID := uuid.UUID(user.ID.Bytes)
|
||||||
|
libraryID := c.QueryParam("library_id")
|
||||||
|
|
||||||
|
// ✅ Add validation
|
||||||
|
if libraryID == "" {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"})
|
||||||
|
}
|
||||||
|
|
||||||
|
libUUID, err := uuid.Parse(libraryID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
|
||||||
|
}
|
||||||
|
prefs, err := h.dashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "Preferences not found"})
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"hidden_collections": prefs.HiddenCollections,
|
||||||
|
"collection_order": prefs.CollectionOrder,
|
||||||
|
"items_per_section": prefs.ItemsPerSection.Int32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+120
-8
@@ -10,6 +10,7 @@ import (
|
|||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
"bookhoard/internal/handlers"
|
"bookhoard/internal/handlers"
|
||||||
"bookhoard/internal/services"
|
"bookhoard/internal/services"
|
||||||
|
"bookhoard/internal/utils"
|
||||||
"bookhoard/templates"
|
"bookhoard/templates"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@@ -137,27 +138,38 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
libUUID, _ := uuid.Parse(libraryID)
|
libUUID, _ := uuid.Parse(libraryID)
|
||||||
userUUID, _ := uuid.Parse(user.ID)
|
userUUID, _ := uuid.Parse(user.ID)
|
||||||
|
|
||||||
prefs, _ := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
prefs, err := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("GetDashboardPreferences failed: %v", err)
|
||||||
|
prefs = database.UserDashboardPreferences{
|
||||||
|
HiddenCollections: []string{},
|
||||||
|
CollectionOrder: []string{},
|
||||||
|
ItemsPerSection: pgtype.Int4{Int32: 20, Valid: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
limit := 20
|
limit := 20
|
||||||
if prefs.ItemsPerSection.Int32 > 0 {
|
if prefs.ItemsPerSection.Int32 > 0 {
|
||||||
limit = int(prefs.ItemsPerSection.Int32)
|
limit = int(prefs.ItemsPerSection.Int32)
|
||||||
}
|
}
|
||||||
var sections []services.DashboardSection
|
|
||||||
sections, err = cfg.DashboardService.GetDashboardSections(
|
// Get ALL sections (unfiltered) for the modal
|
||||||
|
allSections, err := cfg.DashboardService.GetDashboardSections(
|
||||||
c.Request().Context(),
|
c.Request().Context(),
|
||||||
userUUID,
|
userUUID,
|
||||||
libUUID,
|
libUUID,
|
||||||
limit,
|
limit,
|
||||||
prefs.CollectionOrder,
|
prefs.CollectionOrder,
|
||||||
prefs.HiddenCollections,
|
[]string{}, // No filtering - get all sections
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Dashboard sections query failed: %v", err)
|
log.Printf("Dashboard sections query failed: %v", err)
|
||||||
sections = []services.DashboardSection{}
|
allSections = []services.DashboardSection{}
|
||||||
errorMsg = "Error loading dashboard"
|
errorMsg = "Error loading dashboard"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get only visible sections for the dashboard display
|
||||||
|
visibleSections := cfg.DashboardService.FilterHiddenCollections(allSections, prefs.HiddenCollections)
|
||||||
|
|
||||||
userUUID2, _ := uuid.Parse(user.ID)
|
userUUID2, _ := uuid.Parse(user.ID)
|
||||||
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID2))
|
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID2))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -179,10 +191,11 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sectionData := handlers.BuildSections(sections)
|
sectionData := handlers.BuildSections(visibleSections)
|
||||||
|
allSectionsData := handlers.BuildSections(allSections)
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = templates.Dashboard(user, sectionData, libData, libraryID, errorMsg).Render(c.Request().Context(), &buf)
|
err = templates.Dashboard(user, sectionData, allSectionsData, libData, libraryID, prefs.HiddenCollections, limit, errorMsg).Render(c.Request().Context(), &buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -225,6 +238,105 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
return c.HTML(http.StatusOK, buf.String())
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Collection detail page (works for both system and user collections)
|
||||||
|
frontendProtected.GET("/collections/:id", func(c echo.Context) error {
|
||||||
|
user, err := getTemplateUserWithTheme(c, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
||||||
|
}
|
||||||
|
// Parse collection ID from URL
|
||||||
|
collectionID := c.Param("id")
|
||||||
|
collUUID, err := uuid.Parse(collectionID)
|
||||||
|
if err != nil {
|
||||||
|
return renderErrorPage(c, "Invalid collection ID", "invalid_id")
|
||||||
|
}
|
||||||
|
// Fetch collection details
|
||||||
|
collection, err := cfg.Queries.GetCollection(c.Request().Context(), pgtype.UUID{Bytes: collUUID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == "no rows in result set" {
|
||||||
|
return renderErrorPage(c, "Collection not found", "not_found")
|
||||||
|
}
|
||||||
|
return renderErrorPage(c, "Error loading collection", "collection_load_error")
|
||||||
|
}
|
||||||
|
// Fetch books in collection
|
||||||
|
userUUID, _ := uuid.Parse(user.ID)
|
||||||
|
var books []handlers.BookInfo
|
||||||
|
|
||||||
|
if collection.QueryType.Valid && collection.QueryType.String != "" {
|
||||||
|
// System collection - use query type
|
||||||
|
// System collection - need library_id for system collections
|
||||||
|
// Get library_id from query param or default to user's first library
|
||||||
|
libraryID := c.QueryParam("library_id")
|
||||||
|
if libraryID == "" {
|
||||||
|
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
||||||
|
if err == nil && len(libraries) > 0 {
|
||||||
|
libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16])
|
||||||
|
libraryID = libUUID.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
libUUID, _ := uuid.Parse(libraryID)
|
||||||
|
dashboardSvc := services.NewDashboardService(cfg.Queries)
|
||||||
|
sections, err := dashboardSvc.GetDashboardSections(c.Request().Context(), userUUID, libUUID, 1000, []string{}, []string{})
|
||||||
|
if err != nil {
|
||||||
|
return renderErrorPage(c, "Error loading books", "books_load_error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the matching section and convert items
|
||||||
|
for _, section := range sections {
|
||||||
|
if section.CollectionID.String() == collectionID {
|
||||||
|
// Convert []database.MediaItems to []handlers.BookInfo
|
||||||
|
bookCards := make([]handlers.BookInfo, len(section.Items))
|
||||||
|
for i, item := range section.Items {
|
||||||
|
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
||||||
|
bookCards[i] = handlers.BookInfo{
|
||||||
|
MediaItemID: itemUUID.String(),
|
||||||
|
Title: item.Title,
|
||||||
|
Author: getText(item.Author),
|
||||||
|
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
books = bookCards
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User collection - fetch collection items
|
||||||
|
collItems, err := cfg.Queries.GetCollectionItems(c.Request().Context(), pgtype.UUID{Bytes: collUUID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
books = []handlers.BookInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to BookInfo format
|
||||||
|
bookCards := make([]handlers.BookInfo, len(collItems))
|
||||||
|
for i, item := range collItems {
|
||||||
|
itemUUID, _ := uuid.FromBytes(item.MediaItemID.Bytes[0:16])
|
||||||
|
bookCards[i] = handlers.BookInfo{
|
||||||
|
MediaItemID: itemUUID.String(),
|
||||||
|
Title: item.Title,
|
||||||
|
Author: getText(item.Author),
|
||||||
|
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
books = bookCards
|
||||||
|
}
|
||||||
|
// Build collection data
|
||||||
|
colData := templates.CollectionData{
|
||||||
|
ID: collectionID,
|
||||||
|
Name: collection.Name,
|
||||||
|
Description: collection.Description.String,
|
||||||
|
Color: collection.Color.String,
|
||||||
|
Icon: collection.Icon.String,
|
||||||
|
}
|
||||||
|
// Render the CollectionDetail template
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = templates.CollectionDetail(user, colData, books).Render(c.Request().Context(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
})
|
||||||
|
|
||||||
// Custom Section Builder page
|
// Custom Section Builder page
|
||||||
frontendProtected.GET("/custom-section", func(c echo.Context) error {
|
frontendProtected.GET("/custom-section", func(c echo.Context) error {
|
||||||
user, err := getTemplateUserWithTheme(c, cfg)
|
user, err := getTemplateUserWithTheme(c, cfg)
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ func (s *DashboardService) GetDashboardSections(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
results = s.filterHiddenCollections(results, hiddenCollections)
|
results = s.FilterHiddenCollections(results, hiddenCollections)
|
||||||
results = s.reorderCollections(results, collectionOrder)
|
results = s.reorderCollections(results, collectionOrder)
|
||||||
|
|
||||||
if len(collectionOrder) == 0 {
|
if len(collectionOrder) == 0 {
|
||||||
@@ -206,7 +206,7 @@ func (s *DashboardService) GetDashboardSections(
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DashboardService) filterHiddenCollections(sections []DashboardSection, hidden []string) []DashboardSection {
|
func (s *DashboardService) FilterHiddenCollections(sections []DashboardSection, hidden []string) []DashboardSection {
|
||||||
if len(hidden) == 0 {
|
if len(hidden) == 0 {
|
||||||
return sections
|
return sections
|
||||||
}
|
}
|
||||||
@@ -373,6 +373,30 @@ func (s *DashboardService) UpsertDashboardPreferences(ctx context.Context, param
|
|||||||
return s.db.UpsertDashboardPreferences(ctx, params)
|
return s.db.UpsertDashboardPreferences(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DashboardService) SanitizeDashboardPreferences(hiddenCollections, collectionOrder []string) ([]string, []string) {
|
||||||
|
// Deduplicate collection_order while preserving order
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var sanitizedOrder []string
|
||||||
|
for _, id := range collectionOrder {
|
||||||
|
if !seen[id] {
|
||||||
|
seen[id] = true
|
||||||
|
sanitizedOrder = append(sanitizedOrder, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate hidden_collections
|
||||||
|
seen = make(map[string]bool)
|
||||||
|
var sanitizedHidden []string
|
||||||
|
for _, id := range hiddenCollections {
|
||||||
|
if !seen[id] {
|
||||||
|
seen[id] = true
|
||||||
|
sanitizedHidden = append(sanitizedHidden, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitizedHidden, sanitizedOrder
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DashboardService) RestoreSystemCollection(ctx context.Context, userID uuid.UUID, collectionName string, resetType string) error {
|
func (s *DashboardService) RestoreSystemCollection(ctx context.Context, userID uuid.UUID, collectionName string, resetType string) error {
|
||||||
defaultMetadata := map[string]struct {
|
defaultMetadata := map[string]struct {
|
||||||
Description string
|
Description string
|
||||||
|
|||||||
+271
-229
@@ -3,117 +3,122 @@ package templates
|
|||||||
import "bookhoard/internal/handlers"
|
import "bookhoard/internal/handlers"
|
||||||
|
|
||||||
templ Collection(user User, collections []CollectionData, errorMessage string) {
|
templ Collection(user User, collections []CollectionData, errorMessage string) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<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"/>
|
||||||
<title>Collections - Bookhoard</title>
|
<title>Collections - Bookhoard</title>
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/toast.js"></script>
|
<script src="/static/toast.js"></script>
|
||||||
<link href="/static/style.css" rel="stylesheet">
|
<link href="/static/style.css" rel="stylesheet"/>
|
||||||
</head>
|
</head>
|
||||||
<body class="theme-{ user.Theme }">
|
<body class="theme-{ user.Theme }">
|
||||||
@Header(user, "/collections")
|
@Header(user, "/collections")
|
||||||
|
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
<div class="mb-8 flex justify-between items-center">
|
||||||
<div class="mb-8 flex justify-between items-center">
|
<div>
|
||||||
<div>
|
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">My Collections</h1>
|
||||||
<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>
|
||||||
<p style="color: var(--text-secondary)">Organize your books into custom collections</p>
|
</div>
|
||||||
</div>
|
<button onclick="showCreateModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||||||
<button onclick="showCreateModal()" class="btn-primary px-4 py-2 rounded-lg">
|
➕ New Collection
|
||||||
➕ New Collection
|
</button>
|
||||||
</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 id="collections-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="text-center py-16 col-span-full" style="color: var(--text-secondary)">
|
||||||
if len(collections) == 0 {
|
<div class="text-6xl mb-4">📚</div>
|
||||||
<div class="text-center py-16 col-span-full" style="color: var(--text-secondary)">
|
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Collections Yet</h3>
|
||||||
<div class="text-6xl mb-4">📚</div>
|
<p class="mb-4">Create collections to organize your books</p>
|
||||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Collections Yet</h3>
|
<button onclick="showCreateModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||||||
<p class="mb-4">Create collections to organize your books</p>
|
Create Your First Collection
|
||||||
<button onclick="showCreateModal()" class="btn-primary px-4 py-2 rounded-lg">
|
</button>
|
||||||
Create Your First Collection
|
</div>
|
||||||
</button>
|
}
|
||||||
</div>
|
for _, col := range collections {
|
||||||
}
|
<div
|
||||||
|
class="card p-6 rounded-lg border cursor-pointer hover:shadow-lg transition-shadow"
|
||||||
for _, col := range collections {
|
style="background-color: var(--bg-secondary); border-color: { col.Color }; border-left-width: 4px; border-left-style: solid;"
|
||||||
<div class="card p-6 rounded-lg border cursor-pointer hover:shadow-lg transition-shadow"
|
onclick="viewCollection('{ col.ID }')"
|
||||||
style="background-color: var(--bg-secondary); border-color: { col.Color }; border-left-width: 4px; border-left-style: solid;"
|
>
|
||||||
onclick="viewCollection('{ col.ID }')">
|
<div class="flex justify-between items-start mb-4">
|
||||||
<div class="flex justify-between items-start mb-4">
|
<div class="text-3xl">{ col.Icon }</div>
|
||||||
<div class="text-3xl">{ col.Icon }</div>
|
<div class="flex space-x-2">
|
||||||
<div class="flex space-x-2">
|
<button
|
||||||
<button onclick="event.stopPropagation(); editCollection('{ col.ID }')"
|
onclick="event.stopPropagation(); editCollection('{ col.ID }')"
|
||||||
class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
class="p-2 hover:opacity-80 rounded"
|
||||||
✏️
|
style="color: var(--text-secondary); background-color: var(--bg-primary);"
|
||||||
</button>
|
>
|
||||||
<button onclick="event.stopPropagation(); deleteCollection('{ col.ID }')"
|
✏️
|
||||||
class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
</button>
|
||||||
🗑️
|
<button
|
||||||
</button>
|
onclick="event.stopPropagation(); deleteCollection('{ col.ID }')"
|
||||||
</div>
|
class="p-2 hover:opacity-80 rounded"
|
||||||
</div>
|
style="color: var(--text-secondary); background-color: var(--bg-primary);"
|
||||||
<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>
|
🗑️
|
||||||
</div>
|
</button>
|
||||||
}
|
</div>
|
||||||
</div>
|
</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>
|
||||||
<div id="create-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
</div>
|
||||||
<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">
|
</div>
|
||||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Collection</h2>
|
</div>
|
||||||
<button onclick="hideCreateModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
<div id="create-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||||
</div>
|
<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">
|
||||||
<form id="create-form" onsubmit="handleCreate(event)">
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Collection</h2>
|
||||||
<div class="mb-4">
|
<button onclick="hideCreateModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name</label>
|
</div>
|
||||||
<input type="text" id="collection-name" required
|
<form id="create-form" onsubmit="handleCreate(event)">
|
||||||
class="w-full px-4 py-2 border rounded-lg"
|
<div class="mb-4">
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name</label>
|
||||||
placeholder="My Reading List">
|
<input
|
||||||
</div>
|
type="text"
|
||||||
|
id="collection-name"
|
||||||
<div class="mb-4">
|
required
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label>
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
<textarea id="collection-description"
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
class="w-full px-4 py-2 border rounded-lg"
|
placeholder="My Reading List"
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
/>
|
||||||
placeholder="Optional description"
|
</div>
|
||||||
rows="3"></textarea>
|
<div class="mb-4">
|
||||||
</div>
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label>
|
||||||
|
<textarea
|
||||||
<div class="mb-6">
|
id="collection-description"
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Color</label>
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
<div class="flex gap-2">
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
<button type="button" onclick="selectColor('#7aa2f7')" class="w-8 h-8 rounded-full color-option" style="background-color: #7aa2f7;"></button>
|
placeholder="Optional description"
|
||||||
<button type="button" onclick="selectColor('#f7768e')" class="w-8 h-8 rounded-full color-option" style="background-color: #f7768e;"></button>
|
rows="3"
|
||||||
<button type="button" onclick="selectColor('#e0af68')" class="w-8 h-8 rounded-full color-option" style="background-color: #e0af68;"></button>
|
></textarea>
|
||||||
<button type="button" onclick="selectColor('#9ece6a')" class="w-8 h-8 rounded-full color-option" style="background-color: #9ece6a;"></button>
|
</div>
|
||||||
<button type="button" onclick="selectColor('#7dcfff')" class="w-8 h-8 rounded-full color-option" style="background-color: #7dcfff;"></button>
|
<div class="mb-6">
|
||||||
<button type="button" onclick="selectColor('#bb9af7')" class="w-8 h-8 rounded-full color-option" style="background-color: #bb9af7;"></button>
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Color</label>
|
||||||
</div>
|
<div class="flex gap-2">
|
||||||
<input type="hidden" id="collection-color" value="#7aa2f7">
|
<button type="button" onclick="selectColor('#7aa2f7')" class="w-8 h-8 rounded-full color-option" style="background-color: #7aa2f7;"></button>
|
||||||
</div>
|
<button type="button" onclick="selectColor('#f7768e')" class="w-8 h-8 rounded-full color-option" style="background-color: #f7768e;"></button>
|
||||||
|
<button type="button" onclick="selectColor('#e0af68')" class="w-8 h-8 rounded-full color-option" style="background-color: #e0af68;"></button>
|
||||||
<div class="flex justify-end space-x-3">
|
<button type="button" onclick="selectColor('#9ece6a')" class="w-8 h-8 rounded-full color-option" style="background-color: #9ece6a;"></button>
|
||||||
<button type="button" onclick="hideCreateModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
<button type="button" onclick="selectColor('#7dcfff')" class="w-8 h-8 rounded-full color-option" style="background-color: #7dcfff;"></button>
|
||||||
Cancel
|
<button type="button" onclick="selectColor('#bb9af7')" class="w-8 h-8 rounded-full color-option" style="background-color: #bb9af7;"></button>
|
||||||
</button>
|
</div>
|
||||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
<input type="hidden" id="collection-color" value="#7aa2f7"/>
|
||||||
Create Collection
|
</div>
|
||||||
</button>
|
<div class="flex justify-end space-x-3">
|
||||||
</div>
|
<button type="button" onclick="hideCreateModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||||
</form>
|
Cancel
|
||||||
</div>
|
</button>
|
||||||
</div>
|
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||||||
|
Create Collection
|
||||||
<script>
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
let selectedColor = '#7aa2f7';
|
let selectedColor = '#7aa2f7';
|
||||||
|
|
||||||
function showCreateModal() {
|
function showCreateModal() {
|
||||||
@@ -207,123 +212,162 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
|
|||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ErrorToast(errorMessage)
|
||||||
@ErrorToast(errorMessage)
|
</body>
|
||||||
</body>
|
</html>
|
||||||
</html>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
templ CollectionDetail(user User, collection CollectionData, books []handlers.BookInfo) {
|
templ CollectionDetail(user User, collection CollectionData, books []handlers.BookInfo) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<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"/>
|
||||||
<title>{ collection.Name } - Bookhoard</title>
|
<title>{ collection.Name } - Bookhoard</title>
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/toast.js"></script>
|
<script src="/static/toast.js"></script>
|
||||||
<link href="/static/style.css" rel="stylesheet">
|
<link href="/static/style.css" rel="stylesheet"/>
|
||||||
</head>
|
</head>
|
||||||
<body class="theme-{ user.Theme }">
|
<body class="theme-{ user.Theme }">
|
||||||
@Header(user, "/collections")
|
@Header(user, "/collections")
|
||||||
|
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
<div class="mb-6">
|
||||||
<div class="mb-6">
|
<button onclick="backToCollections()" class="btn-secondary px-4 py-2 rounded-lg mb-4">
|
||||||
<button onclick="backToCollections()" class="btn-secondary px-4 py-2 rounded-lg mb-4">
|
← Back to Collections
|
||||||
← Back to Collections
|
</button>
|
||||||
</button>
|
<div class="flex items-center gap-4">
|
||||||
<div class="flex items-center gap-4">
|
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div>
|
||||||
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div>
|
<div>
|
||||||
<div>
|
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1>
|
||||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1>
|
<p style="color: var(--text-secondary)">{ collection.Description }</p>
|
||||||
<p style="color: var(--text-secondary)">{ collection.Description }</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="mb-6 flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
<div class="mb-6 flex justify-between items-center">
|
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Books in this Collection</h2>
|
||||||
<div class="flex items-center gap-4">
|
<span id="selected-count" class="hidden px-3 py-1 text-sm rounded" style="background-color: var(--accent); color: var(--bg-primary);">
|
||||||
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Books in this Collection</h2>
|
0 selected
|
||||||
<span id="selected-count" class="hidden px-3 py-1 text-sm rounded" style="background-color: var(--accent); color: var(--bg-primary);">
|
</span>
|
||||||
0 selected
|
</div>
|
||||||
</span>
|
<div class="flex gap-3">
|
||||||
</div>
|
<div class="flex-1 max-w-md">
|
||||||
<div class="flex gap-3">
|
<input
|
||||||
<div class="flex-1 max-w-md">
|
type="text"
|
||||||
<input type="text" id="collection-search" placeholder="Search within collection..."
|
id="collection-search"
|
||||||
onkeyup="filterCollectionBooks()"
|
placeholder="Search within collection..."
|
||||||
class="w-full px-4 py-2 border rounded-lg"
|
onkeyup="filterCollectionBooks()"
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
</div>
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
<button id="bulk-remove-btn" onclick="removeSelectedBooks()" disabled
|
/>
|
||||||
class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed">
|
</div>
|
||||||
🗑️ Remove Selected
|
<button
|
||||||
</button>
|
id="bulk-remove-btn"
|
||||||
<button onclick="showAddBooksModal()" class="btn-primary px-4 py-2 rounded-lg">
|
onclick="removeSelectedBooks()"
|
||||||
➕ Add Books
|
disabled
|
||||||
</button>
|
class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
</div>
|
>
|
||||||
</div>
|
🗑️ Remove Selected
|
||||||
|
</button>
|
||||||
<div id="books-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<button onclick="showAddBooksModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||||||
if len(books) == 0 {
|
➕ Add Books
|
||||||
<div id="empty-state" class="col-span-full text-center py-16" style="color: var(--text-secondary)">No books in this collection yet.</div>
|
</button>
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
for _, book := range books {
|
<div id="books-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<div class="card p-4 rounded-lg border hover:shadow-lg transition-shadow"
|
if len(books) == 0 {
|
||||||
style="background-color: var(--bg-secondary); border-color: var(--border);">
|
<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 class="flex items-start gap-4">
|
}
|
||||||
<input type="checkbox"
|
for _, book := range books {
|
||||||
onchange="toggleBookForRemoval('{ book.MediaItemID }')"
|
<div
|
||||||
class="w-5 h-5 mt-2">
|
class="card p-4 rounded-lg border hover:shadow-lg transition-shadow"
|
||||||
<div class="aspect-w-3 aspect-h-4 flex-shrink-0 w-24 mb-3 overflow-hidden rounded">
|
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||||
<img src="{ book.CoverImagePath }" alt="Cover"
|
>
|
||||||
class="w-full h-32 object-cover rounded"
|
<div class="flex gap-4">
|
||||||
onerror="this.src='/static/placeholder-book.svg'">
|
<!-- Checkbox and Book Info -->
|
||||||
</div>
|
<div class="flex-shrink-0 pt-1">
|
||||||
<div class="flex-1">
|
<input
|
||||||
<h3 class="font-semibold text-lg mb-1 line-clamp-2" style="color: var(--text-primary)">{ book.Title }</h3>
|
type="checkbox"
|
||||||
if book.Author != "" {
|
onchange="toggleBookForRemoval('{ book.MediaItemID }')"
|
||||||
<p class="text-sm" style="color: var(--text-secondary)">by { book.Author }</p>
|
class="w-5 h-5"
|
||||||
}
|
/>
|
||||||
<button onclick="removeBook('{ book.MediaItemID }')"
|
</div>
|
||||||
class="mt-2 px-3 py-1 text-sm border rounded hover:opacity-80"
|
<div class="flex-1 min-w-0">
|
||||||
style="border-color: var(--border); color: var(--text-secondary);">
|
<h3
|
||||||
Remove
|
class="font-semibold text-lg mb-1 line-clamp-2"
|
||||||
</button>
|
style="color: var(--text-primary)"
|
||||||
</div>
|
>
|
||||||
</div>
|
{ book.Title }
|
||||||
</div>
|
</h3>
|
||||||
}
|
if book.Author != "" {
|
||||||
</div>
|
<p
|
||||||
</div>
|
class="text-sm line-clamp-1"
|
||||||
|
style="color: var(--text-secondary)"
|
||||||
<div id="add-books-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" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
by { book.Author }
|
||||||
<div class="flex justify-between items-center mb-6">
|
</p>
|
||||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Books to Collection</h2>
|
}
|
||||||
<button onclick="hideAddBooksModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
</div>
|
||||||
</div>
|
<!-- Book Cover -->
|
||||||
<p class="mb-4" style="color: var(--text-secondary)">Search and select books to add to this collection.</p>
|
<div class="flex-shrink-0 w-16 sm:w-20">
|
||||||
<div class="mb-4">
|
if book.CoverImagePath != "" {
|
||||||
<input type="text" id="book-search" placeholder="Search books..."
|
<img
|
||||||
class="w-full px-4 py-2 border rounded-lg"
|
src={ book.CoverImagePath }
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
alt="Cover"
|
||||||
</div>
|
class="w-full aspect-[3/4] object-cover rounded shadow-md"
|
||||||
<div id="book-results" class="max-h-64 overflow-y-auto mb-4"></div>
|
onerror="this.src='/static/placeholder-book.svg'"
|
||||||
<div class="flex justify-end space-x-3">
|
/>
|
||||||
<button type="button" onclick="hideAddBooksModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
} else {
|
||||||
Cancel
|
<img
|
||||||
</button>
|
src="/static/placeholder-book.svg"
|
||||||
<button type="button" onclick="addSelectedBooks()" class="btn-primary px-4 py-2 rounded-lg">
|
alt="Cover"
|
||||||
Add Selected Books
|
class="w-full aspect-[3/4] object-cover rounded shadow-md"
|
||||||
</button>
|
/>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Remove Button -->
|
||||||
<script>
|
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
|
||||||
|
<button
|
||||||
|
onclick="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>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="add-books-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" 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 Books to Collection</h2>
|
||||||
|
<button onclick="hideAddBooksModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||||
|
</div>
|
||||||
|
<p class="mb-4" style="color: var(--text-secondary)">Search and select books to add to this collection.</p>
|
||||||
|
<div class="mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="book-search"
|
||||||
|
placeholder="Search books..."
|
||||||
|
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 id="book-results" class="max-h-64 overflow-y-auto mb-4"></div>
|
||||||
|
<div class="flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="hideAddBooksModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="addSelectedBooks()" class="btn-primary px-4 py-2 rounded-lg">
|
||||||
|
Add Selected Books
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
let collectionId = '{ collection.ID }';
|
let collectionId = '{ collection.ID }';
|
||||||
let selectedBooks = new Set();
|
let selectedBooks = new Set();
|
||||||
let booksToRemove = new Set();
|
let booksToRemove = new Set();
|
||||||
@@ -588,9 +632,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
|||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBooks();
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
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 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><button onclick=\"showCreateModal()\" class=\"btn-primary px-4 py-2 rounded-lg\">➕ New Collection</button></div><div id=\"collections-container\" 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 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><button onclick=\"showCreateModal()\" class=\"btn-primary px-4 py-2 rounded-lg\">➕ New Collection</button></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 {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
|||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 47, Col: 60}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 46, Col: 40}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -70,7 +70,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
|||||||
var templ_7745c5c3_Var3 string
|
var templ_7745c5c3_Var3 string
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name)
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 59, Col: 109}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 64, Col: 91}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -83,7 +83,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
|||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description)
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 60, Col: 103}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 65, Col: 85}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -138,7 +138,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
|||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 222, Col: 32}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 226, Col: 27}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -159,7 +159,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
|||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 236, Col: 95}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 239, Col: 81}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -172,7 +172,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
|||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 238, Col: 107}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 241, Col: 90}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -185,7 +185,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
|||||||
var templ_7745c5c3_Var9 string
|
var templ_7745c5c3_Var9 string
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 239, Col: 88}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 242, Col: 71}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -209,7 +209,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
|||||||
var templ_7745c5c3_Var10 string
|
var templ_7745c5c3_Var10 string
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 286, Col: 131}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 301, Col: 108}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -227,7 +227,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
|||||||
var templ_7745c5c3_Var11 string
|
var templ_7745c5c3_Var11 string
|
||||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 288, Col: 108}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 303, Col: 82}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user