From 07578e0206387374913f1ed70d33ccba244cb1df Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Wed, 5 Aug 2026 16:44:52 -0400 Subject: [PATCH] feat(ui): redesign dashboard, bookshelf, book detail, and auth pages - Dashboard: page heading, bold section headers with accent icon tiles, carousel chevrons as SVG icons with theme-aware gradients (wood-paneling gradient classes preserved), and a cinematic BookCard (hover overlay with a quick-action button). BookCard is now fluid so it fills both the carousel slot and the bookshelf grid. - Bookshelf: the filter wall becomes a search toolbar + a slide-in filter drawer (filtersOpen state added to the bookshelf Alpine component). Every filter input, the tristate cover toggle, tag autocomplete, save/load/clear actions, HTMX search/sort, and pagination are preserved. - Book detail: blurred cover backdrop hero, rounded-2xl cover, bold typography, .btn action bar, progress/metadata cards, chip-style external links. All interactive rating, modals, and data-attrs preserved. - Auth/landing: brand-gradient hero for index/login/register, icon feature cards, data-driven theme select (ThemeOptions). All ids (#theme-select, #result, #auth-result, password-requirement ids) and Alpine init preserved. --- templates/book_detail.templ | 693 ++++++++++++++---------------------- templates/bookshelf.templ | 598 +++++++++++++------------------ templates/dashboard.templ | 141 +++----- templates/index.templ | 154 ++++---- templates/login.templ | 93 +++-- templates/register.templ | 127 ++++--- web/src/bookshelf.ts | 1 + 7 files changed, 757 insertions(+), 1050 deletions(-) diff --git a/templates/book_detail.templ b/templates/book_detail.templ index c32af59..27986c0 100644 --- a/templates/book_detail.templ +++ b/templates/book_detail.templ @@ -19,567 +19,403 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) { - + @Header(user, "/media/{ uuidToString(book.ID) }") -
-
- - ← Back + + -
- -
- -
- 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.ReadingProgress != nil && book.ReadingProgress.Percentage.Valid && book.ReadingProgress.Percentage.Float64 >= 1.0 { - +
+ +
+ if book.CoverImagePath.Valid && book.CoverImagePath.String != "" { + { } else { - + { } - - if book.ActiveConflict != nil || book.ReadingProgress != nil { - +
+ +
+

{ book.Title }

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

+ by { book.Author.String } +

} - - + if book.ReadingProgress != nil && book.ReadingProgress.Percentage.Valid && book.ReadingProgress.Percentage.Float64 >= 1.0 { + + } else { + } - - - -
- -
- - - - - - - - - -
- - if book.CommunityRating.Valid && book.CommunityRating.Float64 > 0 { -
- - Community Rating: - + if book.ActiveConflict != nil || book.ReadingProgress != nil { + + } + + +
+ +
+ + + + + + + + + +
+ + if book.CommunityRating.Valid && book.CommunityRating.Float64 > 0 { +
+ Community: + @templ.Raw(renderStars(int32(book.CommunityRating.Float64))) - - { fmt.Sprintf("%.1f / 10", book.CommunityRating.Float64) } - - -
- } - - if book.Series.Valid && book.Series.String != "" { - - } - - if book.ReadingDirection.Valid && book.ReadingDirection.String != "" && book.ReadingDirection.String != "auto" { -
- - πŸ“– { strings.ToUpper(book.ReadingDirection.String) } - -
- } - -
- - if book.AgeRating.Valid && book.AgeRating.String != "" { - - { book.AgeRating.String } - + { fmt.Sprintf("%.1f / 10", book.CommunityRating.Float64) } +
} - - if book.IsBlackAndWhite.Valid && book.IsBlackAndWhite.Bool { - - B&W - - } - - if book.StoryArc.Valid && book.StoryArc.String != "" { - - πŸ“š { book.StoryArc.String } - - } -
- - if len(book.Tags) > 0 { -
- for _, tag := range book.Tags { + +
+ if book.Series.Valid && book.Series.String != "" { - { tag } + { book.Series.String } + if book.SeriesNumber.Valid && book.SeriesNumber.Int32 > 0 { + #{ book.SeriesNumber.Int32 } + } } + if book.ReadingDirection.Valid && book.ReadingDirection.String != "" && book.ReadingDirection.String != "auto" { + { strings.ToUpper(book.ReadingDirection.String) } + } + if book.AgeRating.Valid && book.AgeRating.String != "" { + { book.AgeRating.String } + } + if book.IsBlackAndWhite.Valid && book.IsBlackAndWhite.Bool { + B&W + } + if book.StoryArc.Valid && book.StoryArc.String != "" { + { book.StoryArc.String } + }
- } - - if book.Description.Valid && book.Description.String != "" { -
-

Synopsis

-
- @UnsafeHTML( - bluemonday.UGCPolicy().Sanitize(book.Description.String), - ).ToComponent() + if len(book.Tags) > 0 { +
+ for _, tag := range book.Tags { + + { tag } + + }
-
- } + } +
- +
+
+ + if book.Description.Valid && book.Description.String != "" { +
+

Synopsis

+
+ @UnsafeHTML(bluemonday.UGCPolicy().Sanitize(book.Description.String)).ToComponent() +
+
+ } if book.Summary.Valid && book.Summary.String != "" && book.Summary.String != book.Description.String { -
+

Comic Summary

-
- @UnsafeHTML( - bluemonday.UGCPolicy().Sanitize(book.Summary.String), - ).ToComponent() +
+ @UnsafeHTML(bluemonday.UGCPolicy().Sanitize(book.Summary.String)).ToComponent()
} - if book.MetadataNotes.Valid && book.MetadataNotes.String != "" { -
+

Metadata Notes

-
- { book.MetadataNotes.String } -
+
{ book.MetadataNotes.String }
} - + if book.ReadingProgress != nil { -
+

Reading Progress

if book.ActiveConflict != nil { -
-

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

+
+ @Icon("alert", "h-5 w-5 shrink-0") +

Progress conflict detected β€” click "Sync Progress" to review and resolve.

} -
-
+
- -
+
-

Progress

-

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

+

Progress

+

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

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

Page

-

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

+

Page

+

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

} if book.ReadingProgress.LastReadAt.Valid {
-

Last Read

-

{ FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) }

+

Last Read

+

{ FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) }

} if book.ReadingProgress.LastSyncSource.Valid {
-

Source

-

{ book.ReadingProgress.LastSyncSource.String }

+

Source

+

{ book.ReadingProgress.LastSyncSource.String }

}
} - -
+ +

Metadata

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

Publisher

-

{ book.Publisher.String }

+

Publisher

+

{ book.Publisher.String }

} if book.DatePublished.Valid {
-

Published

-

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

+

Published

+

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

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

Isbn

-

{ book.Isbn.String }

+

ISBN

+

{ book.Isbn.String }

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

Language

-

{ book.Language.String }

+

Language

+

{ book.Language.String }

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

Edition

-

{ book.Edition.String }

+

Edition

+

{ book.Edition.String }

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

Pages

-

{ book.PageCount.Int32 }

+

Pages

+

{ book.PageCount.Int32 }

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

Genre

-

{ book.Genre.String }

+

Genre

+

{ book.Genre.String }

} - if book.SeriesCount.Valid && book.SeriesCount.Int32 > 0 {
-

Series Count

-

{ book.SeriesCount.Int32 } items

+

Series Count

+

{ book.SeriesCount.Int32 } items

} if book.Volume.Valid && book.Volume.Int32 > 0 {
-

Volume

-

Vol. { book.Volume.Int32 }

+

Volume

+

Vol. { book.Volume.Int32 }

} if book.Imprint.Valid && book.Imprint.String != "" {
-

Imprint

-

{ book.Imprint.String }

+

Imprint

+

{ book.Imprint.String }

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

Copyright Year

-

{ book.CopyrightYear.Int32 }

+

Copyright

+

{ book.CopyrightYear.Int32 }

} - if book.MangaType.Valid && book.MangaType.String != "" && book.MangaType.String != "unknown" {
-

Manga Type

-

{ strings.ReplaceAll(book.MangaType.String, "_", " ") }

+

Manga Type

+

{ strings.ReplaceAll(book.MangaType.String, "_", " ") }

} if book.ScanInformation.Valid && book.ScanInformation.String != "" {
-

Scan Info

+

Scan Info

{ book.ScanInformation.String }

} - if len(book.AlternateInfo) > 0 { -
-

Alternate Series

-

{ string(book.AlternateInfo) }

-
- } if altSeries := getAlternateSeries(book.AlternateInfo); altSeries != "" {
-

Alternate Series

-

{ altSeries }

+

Alternate Series

+

{ altSeries }

} if len(book.Contributors) > 0 {
-

Contributors

-

{ strings.Join(book.Contributors, ", ") }

+

Contributors

+

{ strings.Join(book.Contributors, ", ") }

} -
-

Format

-

{ book.MimeType.String }

+

Format

+

{ book.MimeType.String }

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

File Size

-

{ formatFileSize(book.FileSize.Int64) }

+

File Size

+

{ formatFileSize(book.FileSize.Int64) }

}
- if book.GoodreadsID.Valid || book.OpenlibraryID.Valid || book.GoogleBooksID.Valid || book.Asin.Valid || book.Isbn.Valid || book.WebUrl.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 book.WebUrl.Valid && book.WebUrl.String != "" { - - πŸ”— { getDomainName(book.WebUrl.String) } - - } -
+
+ 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 book.WebUrl.Valid && book.WebUrl.String != "" { + { getDomainName(book.WebUrl.String) } + }
}
- + if len(book.Collections) > 0 { -
+

Collections

-
+
for _, col := range book.Collections { - { col.Icon.String } + { col.Icon.String } { col.Name } } @@ -587,7 +423,6 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
}
- @ProgressSyncModal(user, book) @NotesHighlightsModal(book) @MetadataEditorModal(book) diff --git a/templates/bookshelf.templ b/templates/bookshelf.templ index 3ef859c..c3961d1 100644 --- a/templates/bookshelf.templ +++ b/templates/bookshelf.templ @@ -3,6 +3,7 @@ package templates import ( "bookhoard/internal/database" "bookhoard/internal/handlers" + "fmt" ) templ BookShelf( @@ -11,10 +12,10 @@ templ BookShelf( currentLibraryID string, errorMessage string, savedFilters []database.SavedFilters, - books []handlers.BookInfo, // NEW - limit int, // NEW - offset int, // NEW - count int, // NEW + books []handlers.BookInfo, + limit int, + offset int, + count int, ) { @@ -31,363 +32,287 @@ templ BookShelf( x-init="initBookshelf()" > @Header(user, "/bookshelf") -
- -
-
-
- -
- - +
+ + + +
+ +
+

Saved Filters

+
+ for _, filter := range savedFilters { +
+ + +
} +
+ if len(savedFilters) == 0 { +
+ No saved filters yet +
} -
- -
- - +
+ +
+ +
+
+
+
+

Filters

+
- -
- - - -
- -
- - -
-