fix(ui): show read action on JS-rendered dashboard cards

The dashboard re-renders its sections client-side (library switch,
refresh, saving settings) via renderBookCard in dashboard.ts, which was
still the old markup with no .book-card-action overlay. So the read
button appeared on the server-rendered cards but vanished as soon as the
dashboard re-rendered, while the bookshelf (always templ-rendered) kept
working.

- Rewrite renderBookCard to match the templ BookCard: detail link plus
  the play/read action overlay, routing to the reader or the detail page
  when the book has an active conflict.
- Add has_conflict to the BookInfo TS type and stamp it in the dashboard
  sections API (GetSections) so client-rendered cards can route correctly.
- Add pointer-events-none / group-hover:pointer-events-auto to the
  client-rendered carousel nav buttons so they no longer swallow hover
  over edge cards, matching the templ fix.
This commit is contained in:
2026-08-06 08:40:06 -04:00
parent 7b465ac97e
commit f67232a20b
3 changed files with 47 additions and 25 deletions
+1
View File
@@ -65,6 +65,7 @@ func (h *DashboardHandler) GetSections(c *echo.Context) error {
}
sectionData := BuildSections(sections, libraryID)
sectionData = MarkActiveConflictsSections(c.Request().Context(), h.db, user.ID, sectionData)
return c.JSON(http.StatusOK, map[string]interface{}{"sections": sectionData})
}
+43 -23
View File
@@ -205,7 +205,8 @@ function renderSectionHTML(section: SectionData): string {
<div class="carousel-container relative group">
<button class="carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10
w-12 h-full bg-gradient-to-r from-gray-900 to-transparent
flex items-center justify-start opacity-0 group-hover:opacity-100
flex items-center justify-start opacity-0 pointer-events-none
group-hover:pointer-events-auto group-hover:opacity-100
transition-opacity duration-200"
data-action="scroll-carousel"
data-collection-id="${section.id}"
@@ -230,7 +231,8 @@ function renderSectionHTML(section: SectionData): string {
<button class="carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10
w-12 h-full bg-gradient-to-l from-gray-900 to-transparent
flex items-center justify-end opacity-0 group-hover:opacity-100
flex items-center justify-end opacity-0 pointer-events-none
group-hover:pointer-events-auto group-hover:opacity-100
transition-opacity duration-200"
data-action="scroll-carousel"
data-collection-id="${section.id}"
@@ -268,33 +270,51 @@ function renderDashboardCollections(sections: SectionData[]): void {
}
}
const BOOK_OPEN_ICON =
'<svg class="reader-icon h-5 w-5" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<path d="M12 7v14"></path>' +
'<path d="M3 4h6a3 3 0 0 1 3 3v14a2 2 0 0 0-2-2H3z"></path>' +
'<path d="M21 4h-6a3 3 0 0 0-3 3v14a2 2 0 0 1 2-2h7z"></path>' +
"</svg>";
function renderBookCard(book: BookInfo): string {
const coverUrl = book.cover_image_path || "/static/placeholder-book.svg";
const actionHref = book.has_conflict
? `/media/${book.media_item_id}`
: `/readers/${book.media_item_id}`;
const actionLabel = book.has_conflict
? `Resolve progress conflict for ${book.title}`
: `Read ${book.title}`;
const actionTitle = book.has_conflict
? "Resolve progress conflict"
: "Read";
return `
<a href="/media/${book.media_item_id}">
<div class="book-card flex-shrink-0 w-36 rounded-lg overflow-hidden snap-start cursor-pointer
transition-transform duration-200 hover:scale-105"
data-media-item-id="${book.media_item_id}"
tabindex="0"
role="button"
aria-label="View ${book.title}">
<div class="aspect-[2/3] overflow-hidden shadow-lg
bg-gradient-to-br from-gray-700 to-gray-900">
<img src="${coverUrl}"
alt="${book.title}"
class="w-full h-full object-cover"
loading="lazy"
onerror="this.src='/static/placeholder-book.svg'">
</div>
<div class="book-card-text px-2 py-1 bg-[color-mix(in_srgb,var(--wood-border)_40%,transparent)]">
<h3 class="font-semibold text-base line-clamp-2" style="color: var(--text-primary)">
${book.title}
</h3>
${book.author ? `<p class="text-sm line-clamp-1" style="color: var(--text-secondary)">${book.author}</p>` : ""}
<div class="flex-shrink-0 w-36 sm:w-40 snap-start">
<div class="book-card relative w-full h-full rounded-xl overflow-hidden cursor-pointer">
<a href="/media/${book.media_item_id}" class="block h-full">
<div class="book-card-cover aspect-[2/3] overflow-hidden" style="background-color: color-mix(in srgb, var(--text-primary) 8%, transparent);">
<img src="${coverUrl}"
alt="${book.title}"
class="w-full h-full object-cover"
loading="lazy"
onerror="this.src='/static/placeholder-book.svg'">
</div>
<div class="book-card-meta">
<h3 class="font-semibold text-sm leading-snug line-clamp-2" style="color: var(--text-primary)" title="${book.title}">
${book.title}
</h3>
${book.author ? `<p class="text-xs mt-0.5 line-clamp-1" style="color: var(--text-secondary)" title="${book.author}">${book.author}</p>` : ""}
</div>
</a>
<div class="book-card-action">
<a href="${actionHref}"
class="book-card-action-btn"
aria-label="${actionLabel}"
title="${actionTitle}">${BOOK_OPEN_ICON}</a>
</div>
</div>
</div>
</a>
`;
}
+3 -2
View File
@@ -94,13 +94,14 @@ interface CollectionData {
}
// Matches handlers.BookInfo JSON response (internal/handlers/collections.go:66-71)
// JSON tags: media_item_id, title, author, cover_image_path
// Used in: collections.templ (server-rendered), collections.ts
// JSON tags: media_item_id, title, author, cover_image_path, has_conflict
// Used in: collections.templ (server-rendered), collections.ts, dashboard.ts
interface BookInfo {
media_item_id: string;
title: string;
author: string;
cover_image_path: string;
has_conflict: boolean;
}
// Dashboard type definitions