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 := BuildSections(sections, libraryID)
|
||||||
|
sectionData = MarkActiveConflictsSections(c.Request().Context(), h.db, user.ID, sectionData)
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, map[string]interface{}{"sections": 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">
|
<div class="carousel-container relative group">
|
||||||
<button class="carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10
|
<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
|
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"
|
transition-opacity duration-200"
|
||||||
data-action="scroll-carousel"
|
data-action="scroll-carousel"
|
||||||
data-collection-id="${section.id}"
|
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
|
<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
|
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"
|
transition-opacity duration-200"
|
||||||
data-action="scroll-carousel"
|
data-action="scroll-carousel"
|
||||||
data-collection-id="${section.id}"
|
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 {
|
function renderBookCard(book: BookInfo): string {
|
||||||
const coverUrl = book.cover_image_path || "/static/placeholder-book.svg";
|
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 `
|
return `
|
||||||
<a href="/media/${book.media_item_id}">
|
<div class="flex-shrink-0 w-36 sm:w-40 snap-start">
|
||||||
<div class="book-card flex-shrink-0 w-36 rounded-lg overflow-hidden snap-start cursor-pointer
|
<div class="book-card relative w-full h-full rounded-xl overflow-hidden cursor-pointer">
|
||||||
transition-transform duration-200 hover:scale-105"
|
<a href="/media/${book.media_item_id}" class="block h-full">
|
||||||
data-media-item-id="${book.media_item_id}"
|
<div class="book-card-cover aspect-[2/3] overflow-hidden" style="background-color: color-mix(in srgb, var(--text-primary) 8%, transparent);">
|
||||||
tabindex="0"
|
<img src="${coverUrl}"
|
||||||
role="button"
|
alt="${book.title}"
|
||||||
aria-label="View ${book.title}">
|
class="w-full h-full object-cover"
|
||||||
<div class="aspect-[2/3] overflow-hidden shadow-lg
|
loading="lazy"
|
||||||
bg-gradient-to-br from-gray-700 to-gray-900">
|
onerror="this.src='/static/placeholder-book.svg'">
|
||||||
<img src="${coverUrl}"
|
</div>
|
||||||
alt="${book.title}"
|
<div class="book-card-meta">
|
||||||
class="w-full h-full object-cover"
|
<h3 class="font-semibold text-sm leading-snug line-clamp-2" style="color: var(--text-primary)" title="${book.title}">
|
||||||
loading="lazy"
|
${book.title}
|
||||||
onerror="this.src='/static/placeholder-book.svg'">
|
</h3>
|
||||||
</div>
|
${book.author ? `<p class="text-xs mt-0.5 line-clamp-1" style="color: var(--text-secondary)" title="${book.author}">${book.author}</p>` : ""}
|
||||||
<div class="book-card-text px-2 py-1 bg-[color-mix(in_srgb,var(--wood-border)_40%,transparent)]">
|
</div>
|
||||||
<h3 class="font-semibold text-base line-clamp-2" style="color: var(--text-primary)">
|
</a>
|
||||||
${book.title}
|
<div class="book-card-action">
|
||||||
</h3>
|
<a href="${actionHref}"
|
||||||
${book.author ? `<p class="text-sm line-clamp-1" style="color: var(--text-secondary)">${book.author}</p>` : ""}
|
class="book-card-action-btn"
|
||||||
|
aria-label="${actionLabel}"
|
||||||
|
title="${actionTitle}">${BOOK_OPEN_ICON}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+3
-2
@@ -94,13 +94,14 @@ interface CollectionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Matches handlers.BookInfo JSON response (internal/handlers/collections.go:66-71)
|
// Matches handlers.BookInfo JSON response (internal/handlers/collections.go:66-71)
|
||||||
// JSON tags: media_item_id, title, author, cover_image_path
|
// JSON tags: media_item_id, title, author, cover_image_path, has_conflict
|
||||||
// Used in: collections.templ (server-rendered), collections.ts
|
// Used in: collections.templ (server-rendered), collections.ts, dashboard.ts
|
||||||
interface BookInfo {
|
interface BookInfo {
|
||||||
media_item_id: string;
|
media_item_id: string;
|
||||||
title: string;
|
title: string;
|
||||||
author: string;
|
author: string;
|
||||||
cover_image_path: string;
|
cover_image_path: string;
|
||||||
|
has_conflict: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dashboard type definitions
|
// Dashboard type definitions
|
||||||
|
|||||||
Reference in New Issue
Block a user