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:
@@ -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
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+3
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user