From eb349cbc9566488b9e1b92e1471167aaf6790bb3 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 28 Mar 2026 23:54:39 -0400 Subject: [PATCH] feat: add book detail page with comprehensive metadata display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement SSR-first book detail page at /media/:uuid with complete book information, progress tracking, and interactivity. Features: - Cover image (256x384px) with responsive layout - Complete metadata: title, author, description, publisher, ISBN, language, edition, page count, genre, copyright year, format - External service links (Goodreads, Open Library, Google Books, Amazon) with smart URL fallback: ID → ISBN → Title+Author - Reading progress display with device sources (web/kobo/koreader) - Sync progress modal for conflict resolution - Collections display as clickable badges - Notes/highlights counter with placeholder modal - Rating display (1-10 scale with star rendering) - HTML sanitization for book descriptions using bluemonday Data Structure: - handlers.MediaDetail embeds database.MediaItems for zero duplication - Uses existing database queries (GetMediaItem, GetMediaRating, etc.) - Follows project pattern: no parallel type systems Frontend: - TypeScript modal triggers (book-detail.ts) - Alpine.js for modal interactions - TailwindCSS styling with theme variables - Responsive: cover-left layout, mobile stacks vertically Backend: - Route: GET /media/:uuid (protected) - Handler: inline function in frontend.go following existing pattern - Template: SSR-first with progressive enhancement - Returns HTML only (API uses separate /api/media-items/:id endpoint) Files created: - internal/handlers/media_detail.go - templates/book_detail.templ - templates/book_detail_modals.templ - web/src/book-detail.ts Files modified: - internal/router/frontend.go (add route) - web/src/main.ts (import module) --- internal/handlers/media_detail.go | 21 + internal/router/frontend.go | 114 ++++ templates/book_detail.templ | 377 ++++++++++++ templates/book_detail_modals.templ | 159 +++++ templates/book_detail_modals_templ.go | 328 +++++++++++ templates/book_detail_templ.go | 811 ++++++++++++++++++++++++++ web/src/book-detail.ts | 56 ++ web/src/main.ts | 1 + 8 files changed, 1867 insertions(+) create mode 100644 internal/handlers/media_detail.go create mode 100644 templates/book_detail.templ create mode 100644 templates/book_detail_modals.templ create mode 100644 templates/book_detail_modals_templ.go create mode 100644 templates/book_detail_templ.go create mode 100644 web/src/book-detail.ts diff --git a/internal/handlers/media_detail.go b/internal/handlers/media_detail.go new file mode 100644 index 0000000..c9f472e --- /dev/null +++ b/internal/handlers/media_detail.go @@ -0,0 +1,21 @@ +package handlers + +import "bookhoard/internal/database" + +// MediaDetail embeds database.MediaItems for complete book metadata +// No field duplication - template gets direct access to all database fields +type MediaDetail struct { + database.MediaItems // Embedded - ALL book fields available + + // User-specific data + Rating *database.MediaRatings `json:"rating,omitempty"` + Collections []database.Collections `json:"collections"` + ReadingProgress *database.ReadingProgress `json:"reading_progress,omitempty"` + + // Conflict data (if exists) + ActiveConflict *ConflictDetailResponse `json:"active_conflict,omitempty"` + + // Computed counts + NotesCount int `json:"notes_count"` + HighlightsCount int `json:"highlights_count"` +} diff --git a/internal/router/frontend.go b/internal/router/frontend.go index 022fb14..840c5b9 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -3,6 +3,7 @@ package router import ( "bytes" "context" + "encoding/json" "log" "net/http" "strconv" @@ -994,6 +995,119 @@ func registerFrontendRoutes(cfg *Config) { return c.HTML(http.StatusOK, buf.String()) }) + // Book detail page + frontendProtected.GET("/media/:uuid", func(c *echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return renderErrorPage(c, "Error loading user", "user_load_error") + } + + // Parse media UUID from URL + mediaUUID, err := uuid.Parse(c.Param("uuid")) + if err != nil { + return renderErrorPage(c, "Invalid media ID", "invalid_id") + } + pgMediaUUID := uuidToPGType(mediaUUID) + + // Get user UUID for queries + userUUID, _ := uuid.Parse(user.ID) + pgUserID := uuidToPGType(userUUID) + + // Fetch media item (embeds ALL metadata) + mediaItem, err := cfg.Queries.GetMediaItem(c.Request().Context(), pgMediaUUID) + if err != nil { + if err.Error() == "no rows in result set" { + return renderErrorPage(c, "Book not found", "not_found") + } + return renderErrorPage(c, "Error loading book", "database_error") + } + + // Resolve cover image path + if mediaItem.CoverImagePath.Valid && mediaItem.CoverImagePath.String != "" { + resolvedPath := utils.ResolveMediaURL(mediaItem.LibraryID, mediaItem.CoverImagePath) + mediaItem.CoverImagePath = pgtype.Text{String: resolvedPath, Valid: true} + } + + // Fetch rating + var rating *database.MediaRatings + userRating, err := cfg.Queries.GetMediaRating(c.Request().Context(), database.GetMediaRatingParams{ + MediaItemID: pgMediaUUID, + UserID: pgUserID, + }) + if err == nil { + rating = &userRating + } + + // Fetch collections + collections, _ := cfg.Queries.GetCollectionsForBook(c.Request().Context(), pgMediaUUID) + + // Fetch reading progress + var progress *database.ReadingProgress + readingProgress, err := cfg.Queries.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{ + MediaItemID: pgMediaUUID, + UserID: pgUserID, + }) + if err == nil { + progress = &readingProgress + } + + // Fetch active conflict (if any) + var activeConflict *handlers.ConflictDetailResponse + conflicts, err := cfg.Queries.ListSyncConflictsByMediaItem(c.Request().Context(), + database.ListSyncConflictsByMediaItemParams{ + MediaItemID: pgMediaUUID, + UserID: pgUserID, + }) + if err == nil && len(conflicts) > 0 { + for _, conf := range conflicts { + if conf.ResolutionStatus.Valid && conf.ResolutionStatus.String == "unresolved" { + var conflictData map[string]handlers.ConflictSourceData + if err := json.Unmarshal(conf.ConflictData, &conflictData); err == nil { + activeConflict = &handlers.ConflictDetailResponse{ + ID: uuid.UUID(conf.ID.Bytes).String(), + MediaItemID: uuid.UUID(conf.MediaItemID.Bytes).String(), + MediaItemTitle: mediaItem.Title, + ConflictType: conf.ConflictType, + ConflictData: conflictData, + ResolutionStatus: conf.ResolutionStatus.String, + CreatedAt: conf.CreatedAt.Time, + } + } + break + } + } + } + + // Count notes and highlights + notes, _ := cfg.Queries.GetMediaNotes(c.Request().Context(), database.GetMediaNotesParams{ + MediaItemID: pgMediaUUID, + UserID: pgUserID, + }) + highlights, _ := cfg.Queries.GetMediaHighlights(c.Request().Context(), database.GetMediaHighlightsParams{ + MediaItemID: pgMediaUUID, + UserID: pgUserID, + }) + + // Assemble response (no field duplication!) + detail := handlers.MediaDetail{ + MediaItems: mediaItem, // Embedded - ALL fields available + Rating: rating, + Collections: collections, + ReadingProgress: progress, + ActiveConflict: activeConflict, + NotesCount: len(notes), + HighlightsCount: len(highlights), + } + + // Render template + var buf bytes.Buffer + err = templates.BookDetail(user, detail, "").Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + }) + e.GET("/health", cfg.GetHealth) } diff --git a/templates/book_detail.templ b/templates/book_detail.templ new file mode 100644 index 0000000..74ba5c5 --- /dev/null +++ b/templates/book_detail.templ @@ -0,0 +1,377 @@ +package templates + +import ( + "bookhoard/internal/handlers" + "fmt" + "github.com/microcosm-cc/bluemonday" +) + +templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) { + + + + + + { book.Title } - Bookhoard + + + + + @Header(user, "/media/{ uuidToString(book.ID) }") +
+ +
+ +
+ if book.CoverImagePath.Valid && book.CoverImagePath.String != "" { + { + } else { + { book.Title } + } +
+ +
+ +

{ book.Title }

+ if book.Author.Valid && book.Author.String != "" { +

+ by { book.Author.String } +

+ } + +
+ + + + if book.ActiveConflict != nil || book.ReadingProgress != nil { + + } + + + + +
+ + if book.Rating != nil { +
+ + { renderStars(book.Rating.Rating) } + + + ({ fmt.Sprintf("%.1f", float64(book.Rating.Rating)/2.0) } / 5) + +
+ } + + if book.Series.Valid && book.Series.String != "" { +
+ + { book.Series.String } + if book.SeriesNumber.Valid && book.SeriesNumber.Int32 > 0 { + #{ book.SeriesNumber.Int32 } + } + +
+ } + + if book.Description.Valid && book.Description.String != "" { +
+

Synopsis

+
+ @UnsafeHTML( + bluemonday.UGCPolicy().Sanitize(book.Description.String), + ).ToComponent() +
+
+ } +
+
+ + if book.ReadingProgress != nil { +
+

Reading Progress

+ if book.ActiveConflict != nil { +
+

+ ⚠️ Progress conflict detected - Click "Sync Progress" to review and resolve +

+
+ } + +
+
+
+
+
+ +
+
+

Progress

+

+ { fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64) }% +

+
+ if book.ReadingProgress.CurrentPage.Valid && book.ReadingProgress.TotalPages.Valid { +
+

Page

+

{ book.ReadingProgress.CurrentPage.Int32 } / { book.ReadingProgress.TotalPages.Int32 }

+
+ } + if book.ReadingProgress.LastReadAt.Valid { +
+

Last Read

+

{ book.ReadingProgress.LastReadAt.Time.Format("2006-01-02 15:04") }

+
+ } + if book.ReadingProgress.LastSyncSource.Valid { +
+

Source

+

{ book.ReadingProgress.LastSyncSource.String }

+
+ } +
+
+ } + +
+

Metadata

+
+ + if book.Publisher.Valid && book.Publisher.String != "" { +
+

Publisher

+

{ book.Publisher.String }

+
+ } + if book.DatePublished.Valid { +
+

Published

+

{ book.DatePublished.Time.Format("2006-01-02") }

+
+ } + if book.Isbn.Valid && book.Isbn.String != "" { +
+

Isbn

+

{ book.Isbn.String }

+
+ } + if book.Language.Valid && book.Language.String != "" { +
+

Language

+

{ book.Language.String }

+
+ } + if book.Edition.Valid && book.Edition.String != "" { +
+

Edition

+

{ book.Edition.String }

+
+ } + if book.PageCount.Valid && book.PageCount.Int32 > 0 { +
+

Pages

+

{ book.PageCount.Int32 }

+
+ } + if book.Genre.Valid && book.Genre.String != "" { +
+

Genre

+

{ book.Genre.String }

+
+ } + if book.CopyrightYear.Valid && book.CopyrightYear.Int32 > 0 { +
+

Copyright Year

+

{ book.CopyrightYear.Int32 }

+
+ } + +
+

Format

+

{ book.MimeType.String }

+
+ if book.FileSize.Valid && book.FileSize.Int64 > 0 { +
+

File Size

+

{ formatFileSize(book.FileSize.Int64) }

+
+ } +
+ + if book.GoodreadsID.Valid || book.OpenlibraryID.Valid || book.GoogleBooksID.Valid || book.Asin.Valid || book.Isbn.Valid { +
+

External Links

+
+ if book.GoodreadsID.Valid && book.GoodreadsID.String != "" { + + 📚 Goodreads + + } else { + + 📚 Goodreads + + } + if book.OpenlibraryID.Valid && book.OpenlibraryID.String != "" { + + 📖 Open Library + + } else { + + 📖 Open Library + + } + if book.GoogleBooksID.Valid && book.GoogleBooksID.String != "" { + + 🔍 Google Books + + } else { + + 🔍 Google Books + + } + if book.Asin.Valid && book.Asin.String != "" { + + 🛒 Amazon + + } else if book.Isbn.Valid && book.Isbn.String != "" { + + 🛒 Amazon + + } +
+
+ } +
+ + if len(book.Collections) > 0 { +
+

Collections

+
+ for _, col := range book.Collections { + + { col.Icon.String } + { col.Name } + + } +
+
+ } +
+ + @ProgressSyncModal(book) + @NotesHighlightsModal(book) + @ErrorToast(errorMessage) + + +} diff --git a/templates/book_detail_modals.templ b/templates/book_detail_modals.templ new file mode 100644 index 0000000..672a6ea --- /dev/null +++ b/templates/book_detail_modals.templ @@ -0,0 +1,159 @@ +package templates + +import ( + "bookhoard/internal/handlers" + "fmt" +) + +// ProgressSyncModal shows progress from all devices for manual review +templ ProgressSyncModal(book handlers.MediaDetail) { + +} + +// NotesHighlightsModal - Placeholder for future feature +templ NotesHighlightsModal(book handlers.MediaDetail) { + +} diff --git a/templates/book_detail_modals_templ.go b/templates/book_detail_modals_templ.go new file mode 100644 index 0000000..be66acb --- /dev/null +++ b/templates/book_detail_modals_templ.go @@ -0,0 +1,328 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1001 +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" + +import ( + "bookhoard/internal/handlers" + "fmt" +) + +// ProgressSyncModal shows progress from all devices for manual review +func ProgressSyncModal(book handlers.MediaDetail) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Sync Progress

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 22, Col: 74} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.ActiveConflict != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

⚠️ Conflict Detected

Progress differs between devices. Review the options below and manually resolve via the Conflicts page.

Go to Conflicts Page →
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for source, data := range book.ActiveConflict.ConflictData { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(source) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 65, Col: 18} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Timestamp.Format("2006-01-02 15:04:05")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 68, Col: 56} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", data.Data["percentage"].(float64))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 73, Col: 66} + } + _, 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, "%
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Data["current_page"] != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
Page: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", data.Data["current_page"].(float64))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 80, Col: 73} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " / ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", data.Data["total_pages"].(float64))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 80, Col: 135} + } + _, 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Data["epubcfi"] != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
CFI: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.Data["epubcfi"].(string)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 85, Col: 45} + } + _, 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if book.ReadingProgress != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

No conflicts detected. Current progress from ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastSyncSource.String) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 94, Col: 105} + } + _, 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, ":

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 101, Col: 68} + } + _, 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, "%

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastSyncSource.String) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 104, Col: 50} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.ReadingProgress.CurrentPage.Valid && book.ReadingProgress.TotalPages.Valid { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

Page ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.CurrentPage.Int32) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 108, Col: 52} + } + _, 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, 20, " of ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.TotalPages.Int32) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 108, Col: 97} + } + _, 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, 21, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.ReadingProgress.LastReadAt.Valid { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

Last read: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastReadAt.Time.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 113, Col: 83} + } + _, 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, 23, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// NotesHighlightsModal - Placeholder for future feature +func NotesHighlightsModal(book handlers.MediaDetail) 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_Var15 := templ.GetChildren(ctx) + if templ_7745c5c3_Var15 == nil { + templ_7745c5c3_Var15 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
📝

Notes & Highlights

This book has ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(book.NotesCount) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 145, Col: 43} + } + _, 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, 27, " notes and ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(book.HighlightsCount) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 145, Col: 95} + } + _, 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, 28, " highlights.

Feature coming soon!

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/templates/book_detail_templ.go b/templates/book_detail_templ.go new file mode 100644 index 0000000..7843956 --- /dev/null +++ b/templates/book_detail_templ.go @@ -0,0 +1,811 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1001 +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" + +import ( + "bookhoard/internal/handlers" + "fmt" + "github.com/microcosm-cc/bluemonday" +) + +func BookDetail(user User, book handlers.MediaDetail, errorMessage 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) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 15, 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, " - Bookhoard") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Header(user, "/media/{ uuidToString(book.ID) }").Render(ctx, templ_7745c5c3_Buffer) + 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 + } + if book.CoverImagePath.Valid && book.CoverImagePath.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"")") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"{") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 44, Col: 89} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.Author.Valid && book.Author.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

by ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author.String) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 47, Col: 31} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.ActiveConflict != nil || book.ReadingProgress != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.Rating != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(renderStars(book.Rating.Rating)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 99, Col: 42} + } + _, 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, 19, " (") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", float64(book.Rating.Rating)/2.0)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 102, Col: 64} + } + _, 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, " / 5)
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.Series.Valid && book.Series.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(book.Series.String) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 113, Col: 29} + } + _, 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, 23, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.SeriesNumber.Valid && book.SeriesNumber.Int32 > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "#") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.SeriesNumber.Int32) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 115, Col: 36} + } + _, 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, 25, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.Description.Valid && book.Description.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "

Synopsis

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = UnsafeHTML( + bluemonday.UGCPolicy().Sanitize(book.Description.String), + ).ToComponent().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.ReadingProgress != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "

Reading Progress

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.ActiveConflict != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "

⚠️ Progress conflict detected - Click \"Sync Progress\" to review and resolve

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "

Progress

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 164, Col: 71} + } + _, 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, 33, "%

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.ReadingProgress.CurrentPage.Valid && book.ReadingProgress.TotalPages.Valid { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

Page

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.CurrentPage.Int32) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 170, Col: 52} + } + _, 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, 35, " / ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.TotalPages.Int32) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 170, Col: 96} + } + _, 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, 36, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.ReadingProgress.LastReadAt.Valid { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "

Last Read

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastReadAt.Time.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 176, Col: 77} + } + _, 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, 38, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.ReadingProgress.LastSyncSource.Valid { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

Source

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastSyncSource.String) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 182, Col: 75} + } + _, 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, 40, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "

Metadata

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.Publisher.Valid && book.Publisher.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

Publisher

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(book.Publisher.String) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 199, Col: 34} + } + _, 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, 44, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.DatePublished.Valid { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "

Published

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(book.DatePublished.Time.Format("2006-01-02")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 205, Col: 57} + } + _, 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, 46, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.Isbn.Valid && book.Isbn.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "

Isbn

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(book.Isbn.String) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 211, Col: 29} + } + _, 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, 48, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.Language.Valid && book.Language.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "

Language

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(book.Language.String) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 217, Col: 52} + } + _, 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, 50, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.Edition.Valid && book.Edition.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "

Edition

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(book.Edition.String) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 223, Col: 32} + } + _, 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, 52, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.PageCount.Valid && book.PageCount.Int32 > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "

Pages

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(book.PageCount.Int32) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 229, Col: 33} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.Genre.Valid && book.Genre.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "

Genre

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(book.Genre.String) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 235, Col: 30} + } + _, 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, 56, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.CopyrightYear.Valid && book.CopyrightYear.Int32 > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "

Copyright Year

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(book.CopyrightYear.Int32) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 241, Col: 37} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "

Format

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(book.MimeType.String) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 247, Col: 32} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.FileSize.Valid && book.FileSize.Int64 > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "

File Size

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(formatFileSize(book.FileSize.Int64)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 252, Col: 48} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.GoodreadsID.Valid || book.OpenlibraryID.Valid || book.GoogleBooksID.Valid || book.Asin.Valid || book.Isbn.Valid { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "

External Links

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if book.GoodreadsID.Valid && book.GoodreadsID.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "📚 Goodreads ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "📚 Goodreads ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.OpenlibraryID.Valid && book.OpenlibraryID.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "📖 Open Library ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "📖 Open Library ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.GoogleBooksID.Valid && book.GoogleBooksID.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "🔍 Google Books ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "🔍 Google Books ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if book.Asin.Valid && book.Asin.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "🛒 Amazon") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if book.Isbn.Valid && book.Isbn.String != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "🛒 Amazon") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(book.Collections) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ProgressSyncModal(book).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = NotesHighlightsModal(book).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ErrorToast(errorMessage).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/web/src/book-detail.ts b/web/src/book-detail.ts new file mode 100644 index 0000000..1f39908 --- /dev/null +++ b/web/src/book-detail.ts @@ -0,0 +1,56 @@ +import { Alpine } from "./alpine"; +import { showToast } from "./toast"; + +function showReaderPlaceholder(): void { + showToast("Ebook reader coming soon!", "info"); +} + +function showMetadataEditorPlaceholder(): void { + showToast("Metadata editor coming soon!", "info"); +} + +function showProgressSyncModal(): void { + const modal = document.getElementById("progress-sync-modal"); + if (modal) { + modal.classList.remove("hidden"); + } +} + +function showNotesModal(): void { + const modal = document.getElementById("notes-modal"); + if (modal) { + modal.classList.remove("hidden"); + } +} + +function hideProgressSyncModal(): void { + const modal = document.getElementById("progress-sync-modal"); + if (modal) { + modal.classList.add("hidden"); + } +} + +function hideNotesModal(): void { + const modal = document.getElementById("notes-modal"); + if (modal) { + modal.classList.add("hidden"); + } +} + +export { + showReaderPlaceholder, + showMetadataEditorPlaceholder, + showProgressSyncModal, + showNotesModal, + hideProgressSyncModal, + hideNotesModal, +}; + +Alpine.data("bookDetail", () => ({ + showReaderPlaceholder, + showMetadataEditorPlaceholder, + showProgressSyncModal, + showNotesModal, + hideProgressSyncModal, + hideNotesModal, +})); diff --git a/web/src/main.ts b/web/src/main.ts index 957a223..4a170c9 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -8,6 +8,7 @@ import "./api-explorer"; import "./api-explorer-docs"; import "./bookPicker"; import "./bookshelf"; +import "./book-detail"; import "./collection-rules"; import "./collections"; import "./conflicts";