Bring the tighten-ui brand treatment into new-ui:
- Replace the emoji book (📚) in the sidebar header with the book-open
icon rendered in the theme accent color, matching the tighten-ui
header brand (templates/header.templ).
- Add web/static/favicon.svg (book-open glyph, tokyo-night accent
#7aa2f7 stroke) and reference it from the <head> of all 27 page
templates, so the favicon is present on login/setup/error pages too.
- Regenerate templ output for all affected templates.
434 lines
22 KiB
Templ
434 lines
22 KiB
Templ
package templates
|
|
|
|
import (
|
|
"bookhoard/internal/handlers"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"github.com/microcosm-cc/bluemonday"
|
|
"strings"
|
|
)
|
|
|
|
templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>{ book.Title } - Bookhoard</title>
|
|
<script src="/static/htmx.min.js"></script>
|
|
<link href="/static/style.css" rel="stylesheet"/>
|
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
|
</head>
|
|
<body
|
|
x-data="bookDetail"
|
|
class="theme-{ user.Theme }"
|
|
data-format-group={ book.FormatGroup }
|
|
data-library-id={ uuidToString(book.LibraryID) }
|
|
data-rating={ fmt.Sprintf("%d", getBookRating(book.Rating)) }
|
|
data-conflict-id={ conflictID(book.ActiveConflict) }
|
|
data-conflict-winner={ conflictWinnerSource(book.ActiveConflict) }
|
|
>
|
|
@Header(user, "/media/{ uuidToString(book.ID) }")
|
|
<!-- Hero with blurred cover backdrop -->
|
|
<div class="relative overflow-hidden">
|
|
if book.CoverImagePath.Valid && book.CoverImagePath.String != "" {
|
|
<div class="hero-backdrop" style={ "background-image: url('" + book.CoverImagePath.String + "')" }></div>
|
|
}
|
|
<div class="relative z-10 xs:w-full md:mx-auto md:container px-4 sm:px-6 lg:px-8 pt-6 pb-8">
|
|
<a id="back-link" href="/dashboard" class="inline-flex items-center gap-1.5 text-sm font-medium hover:underline transition-colors mb-6" style="color: var(--text-secondary);">
|
|
@Icon("arrow-left", "h-4 w-4")
|
|
<span>Back</span>
|
|
</a>
|
|
<div class="flex flex-col md:flex-row gap-8">
|
|
<!-- Cover -->
|
|
<div class="flex-shrink-0 mx-auto md:mx-0">
|
|
if book.CoverImagePath.Valid && book.CoverImagePath.String != "" {
|
|
<img
|
|
src={ book.CoverImagePath.String }
|
|
alt={ book.Title }
|
|
class="w-48 md:w-56 h-72 md:h-80 object-cover rounded-2xl"
|
|
style="box-shadow: var(--shadow-pop);"
|
|
onerror="this.src='/static/placeholder-book.svg'"
|
|
/>
|
|
} else {
|
|
<img
|
|
src="/static/placeholder-book.svg"
|
|
alt={ book.Title }
|
|
class="w-48 md:w-56 h-72 md:h-80 object-cover rounded-2xl"
|
|
style="box-shadow: var(--shadow-pop);"
|
|
/>
|
|
}
|
|
</div>
|
|
<!-- Details -->
|
|
<div class="flex-1 min-w-0">
|
|
<h1 class="text-3xl md:text-4xl font-bold tracking-tight mb-2" style="color: var(--text-primary)">{ book.Title }</h1>
|
|
if book.Author.Valid && book.Author.String != "" {
|
|
<h2 class="text-lg md:text-xl mb-5" style="color: var(--text-secondary)">
|
|
by { book.Author.String }
|
|
</h2>
|
|
}
|
|
<!-- Action buttons -->
|
|
<div class="flex flex-wrap gap-2.5 mb-6">
|
|
<button
|
|
@click={ "window.location.href = '/readers/" + uuidToString(book.ID) + "'" }
|
|
class="btn btn-primary px-5 py-2.5"
|
|
>
|
|
@Icon("book-open", "h-4 w-4")
|
|
<span>Read Now</span>
|
|
</button>
|
|
if book.ReadingProgress != nil && book.ReadingProgress.Percentage.Valid && book.ReadingProgress.Percentage.Float64 >= 1.0 {
|
|
<button @click="toggleRead(false)" :disabled="readSaving" class="btn btn-secondary px-5 py-2.5">
|
|
<span x-show="!readSaving" class="inline-flex items-center gap-2">@Icon("check-circle", "h-4 w-4")<span>Mark Unread</span></span>
|
|
<svg x-show="readSaving" class="animate-spin inline-block h-5 w-5" viewBox="0 0 24 24" fill="none">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
|
</svg>
|
|
</button>
|
|
} else {
|
|
<button @click="toggleRead(true)" :disabled="readSaving" class="btn btn-secondary px-5 py-2.5">
|
|
<span x-show="!readSaving" class="inline-flex items-center gap-2">@Icon("check", "h-4 w-4")<span>Mark Read</span></span>
|
|
<svg x-show="readSaving" class="animate-spin inline-block h-5 w-5" viewBox="0 0 24 24" fill="none">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
|
</svg>
|
|
</button>
|
|
}
|
|
if book.ActiveConflict != nil || book.ReadingProgress != nil {
|
|
<button @click="showProgressSyncModal()" class="btn btn-secondary px-5 py-2.5">
|
|
@Icon("sync", "h-4 w-4")
|
|
<span>Sync Progress</span>
|
|
</button>
|
|
}
|
|
<button @click="showNotesModal()" class="btn btn-secondary px-5 py-2.5">
|
|
@Icon("edit", "h-4 w-4")
|
|
<span>Notes</span>
|
|
if book.NotesCount + book.HighlightsCount > 0 {
|
|
<span class="badge ml-1" style="background-color: var(--accent-muted); color: var(--accent);">{ book.NotesCount + book.HighlightsCount }</span>
|
|
}
|
|
</button>
|
|
<button @click="showMetadataEditor()" class="btn btn-secondary px-5 py-2.5">
|
|
@Icon("edit", "h-4 w-4")
|
|
<span>Edit</span>
|
|
</button>
|
|
</div>
|
|
<!-- Rating -->
|
|
<div class="mb-5" @mouseleave="ratingHover = 0">
|
|
<span class="text-2xl">
|
|
<template x-for="i in 5" :key="i">
|
|
<span style="position: relative; display: inline-block;">
|
|
<span :style="starFill(i)">★</span>
|
|
<button
|
|
type="button"
|
|
@click="setRating(i*2-1)"
|
|
@mouseenter="ratingHover = i*2-1"
|
|
:disabled="ratingSaving"
|
|
:aria-label="'Rate ' + ((i*2-1)/2) + ' of 5 stars'"
|
|
style="position: absolute; left: 0; top: 0; width: 50%; height: 100%; background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer;"
|
|
></button>
|
|
<button
|
|
type="button"
|
|
@click="setRating(i*2)"
|
|
@mouseenter="ratingHover = i*2"
|
|
:disabled="ratingSaving"
|
|
:aria-label="'Rate ' + ((i*2)/2) + ' of 5 stars'"
|
|
style="position: absolute; right: 0; top: 0; width: 50%; height: 100%; background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer;"
|
|
></button>
|
|
</span>
|
|
</template>
|
|
</span>
|
|
<span class="ml-2 text-sm align-middle" style="color: var(--text-secondary);" x-text="ratingText()"></span>
|
|
<button
|
|
type="button"
|
|
x-show="userRating > 0 && !ratingSaving"
|
|
@click="clearRating()"
|
|
class="ml-2 text-xs underline hover:opacity-70"
|
|
style="color: var(--text-secondary);"
|
|
>Clear</button>
|
|
<svg x-show="ratingSaving" class="animate-spin inline-block h-4 w-4 ml-1 align-middle" viewBox="0 0 24 24" fill="none" style="color: var(--text-secondary);">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
|
</svg>
|
|
</div>
|
|
<!-- Community rating -->
|
|
if book.CommunityRating.Valid && book.CommunityRating.Float64 > 0 {
|
|
<div class="mb-3 text-sm" style="color: var(--text-secondary);">
|
|
Community:
|
|
<span class="font-bold ml-1" style="color: var(--text-primary);">
|
|
@templ.Raw(renderStars(int32(book.CommunityRating.Float64)))
|
|
</span>
|
|
<span class="ml-2">{ fmt.Sprintf("%.1f / 10", book.CommunityRating.Float64) }</span>
|
|
</div>
|
|
}
|
|
<!-- Badges: series, direction, tags -->
|
|
<div class="flex flex-wrap items-center gap-2 mb-3">
|
|
if book.Series.Valid && book.Series.String != "" {
|
|
<a
|
|
href={ "/series/detail?name=" + url.QueryEscape(book.Series.String) + "&library_id=" + uuidToString(book.LibraryID) }
|
|
class="chip hover:opacity-90"
|
|
style="background-color: var(--accent); color: var(--bg-primary); text-decoration: none;"
|
|
>
|
|
{ book.Series.String }
|
|
if book.SeriesNumber.Valid && book.SeriesNumber.Int32 > 0 {
|
|
#{ book.SeriesNumber.Int32 }
|
|
}
|
|
</a>
|
|
}
|
|
if book.ReadingDirection.Valid && book.ReadingDirection.String != "" && book.ReadingDirection.String != "auto" {
|
|
<span class="chip" style="background-color: var(--accent); color: var(--bg-primary);">{ strings.ToUpper(book.ReadingDirection.String) }</span>
|
|
}
|
|
if book.AgeRating.Valid && book.AgeRating.String != "" {
|
|
<span class="badge" style="border: 1px solid var(--border); color: var(--text-secondary);">{ book.AgeRating.String }</span>
|
|
}
|
|
if book.IsBlackAndWhite.Valid && book.IsBlackAndWhite.Bool {
|
|
<span class="badge" style="border: 1px solid var(--border); color: var(--text-secondary);">B&W</span>
|
|
}
|
|
if book.StoryArc.Valid && book.StoryArc.String != "" {
|
|
<span class="badge" style="border: 1px solid var(--border); color: var(--text-secondary);">{ book.StoryArc.String }</span>
|
|
}
|
|
</div>
|
|
if len(book.Tags) > 0 {
|
|
<div class="flex flex-wrap gap-2 mb-4">
|
|
for _, tag := range book.Tags {
|
|
<a
|
|
href={ "/tags/detail?name=" + url.QueryEscape(tag) + "&library_id=" + uuidToString(book.LibraryID) }
|
|
class="badge hover:opacity-80 transition-opacity"
|
|
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary); text-decoration: none;"
|
|
>
|
|
{ tag }
|
|
</a>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="xs:w-full md:mx-auto md:container px-4 sm:px-6 lg:px-8 pb-10">
|
|
<!-- Synopsis -->
|
|
if book.Description.Valid && book.Description.String != "" {
|
|
<div class="card p-6 mb-6">
|
|
<h3 class="font-semibold mb-2" style="color: var(--text-primary)">Synopsis</h3>
|
|
<div class="max-h-[20rem] overflow-y-auto pr-2 text-sm leading-relaxed" style="color: var(--text-secondary)">
|
|
@UnsafeHTML(bluemonday.UGCPolicy().Sanitize(book.Description.String)).ToComponent()
|
|
</div>
|
|
</div>
|
|
}
|
|
if book.Summary.Valid && book.Summary.String != "" && book.Summary.String != book.Description.String {
|
|
<div class="card p-6 mb-6">
|
|
<h3 class="font-semibold mb-2" style="color: var(--text-primary)">Comic Summary</h3>
|
|
<div class="max-h-[20rem] overflow-y-auto pr-2 text-sm leading-relaxed" style="color: var(--text-secondary)">
|
|
@UnsafeHTML(bluemonday.UGCPolicy().Sanitize(book.Summary.String)).ToComponent()
|
|
</div>
|
|
</div>
|
|
}
|
|
if book.MetadataNotes.Valid && book.MetadataNotes.String != "" {
|
|
<div class="card p-6 mb-6">
|
|
<h3 class="font-semibold mb-2" style="color: var(--text-primary)">Metadata Notes</h3>
|
|
<div class="text-sm" style="color: var(--text-secondary);">{ book.MetadataNotes.String }</div>
|
|
</div>
|
|
}
|
|
<!-- Progress -->
|
|
if book.ReadingProgress != nil {
|
|
<div class="card p-6 mb-6">
|
|
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Reading Progress</h3>
|
|
if book.ActiveConflict != nil {
|
|
<div class="mb-4 p-3 rounded-xl flex items-start gap-2" style="background-color: color-mix(in srgb, var(--status-warning) 14%, transparent); border: 1px solid var(--status-warning);">
|
|
@Icon("alert", "h-5 w-5 shrink-0")
|
|
<p class="text-sm" style="color: var(--text-primary);">Progress conflict detected — click "Sync Progress" to review and resolve.</p>
|
|
</div>
|
|
}
|
|
<div class="mb-4">
|
|
<div class="w-full rounded-full h-2.5" style="background-color: var(--bg-primary);">
|
|
<div
|
|
class="h-2.5 rounded-full transition-all"
|
|
style={ fmt.Sprintf("width: %.1f%%; background-color: var(--accent);", book.ReadingProgress.Percentage.Float64*100) }
|
|
></div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Progress</p>
|
|
<p class="text-2xl font-bold" style="color: var(--accent);">{ fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64*100) }%</p>
|
|
</div>
|
|
if book.ReadingProgress.CurrentPage.Valid && book.ReadingProgress.TotalPages.Valid {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Page</p>
|
|
<p style="color: var(--text-primary)">{ book.ReadingProgress.CurrentPage.Int32 } / { book.ReadingProgress.TotalPages.Int32 }</p>
|
|
</div>
|
|
}
|
|
if book.ReadingProgress.LastReadAt.Valid {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Last Read</p>
|
|
<p class="text-sm" style="color: var(--text-primary)">{ FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) }</p>
|
|
</div>
|
|
}
|
|
if book.ReadingProgress.LastSyncSource.Valid {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Source</p>
|
|
<p class="capitalize" style="color: var(--text-primary)">{ book.ReadingProgress.LastSyncSource.String }</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
<!-- Metadata grid -->
|
|
<div class="card p-6 mb-6">
|
|
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Metadata</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-4 text-sm">
|
|
if book.Publisher.Valid && book.Publisher.String != "" {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Publisher</p>
|
|
<p style="color: var(--text-primary)">{ book.Publisher.String }</p>
|
|
</div>
|
|
}
|
|
if book.DatePublished.Valid {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Published</p>
|
|
<p style="color: var(--text-primary)">{ book.DatePublished.Time.Format("01-02-2006") }</p>
|
|
</div>
|
|
}
|
|
if book.Isbn.Valid && book.Isbn.String != "" {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">ISBN</p>
|
|
<p style="color: var(--text-primary)">{ book.Isbn.String }</p>
|
|
</div>
|
|
}
|
|
if book.Language.Valid && book.Language.String != "" {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Language</p>
|
|
<p class="capitalize" style="color: var(--text-primary)">{ book.Language.String }</p>
|
|
</div>
|
|
}
|
|
if book.Edition.Valid && book.Edition.String != "" {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Edition</p>
|
|
<p style="color: var(--text-primary)">{ book.Edition.String }</p>
|
|
</div>
|
|
}
|
|
if book.PageCount.Valid && book.PageCount.Int32 > 0 {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Pages</p>
|
|
<p style="color: var(--text-primary)">{ book.PageCount.Int32 }</p>
|
|
</div>
|
|
}
|
|
if book.Genre.Valid && book.Genre.String != "" {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Genre</p>
|
|
<p style="color: var(--text-primary)">{ book.Genre.String }</p>
|
|
</div>
|
|
}
|
|
if book.SeriesCount.Valid && book.SeriesCount.Int32 > 0 {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Series Count</p>
|
|
<p style="color: var(--text-primary)">{ book.SeriesCount.Int32 } items</p>
|
|
</div>
|
|
}
|
|
if book.Volume.Valid && book.Volume.Int32 > 0 {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Volume</p>
|
|
<p style="color: var(--text-primary)">Vol. { book.Volume.Int32 }</p>
|
|
</div>
|
|
}
|
|
if book.Imprint.Valid && book.Imprint.String != "" {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Imprint</p>
|
|
<p style="color: var(--text-primary)">{ book.Imprint.String }</p>
|
|
</div>
|
|
}
|
|
if book.CopyrightYear.Valid && book.CopyrightYear.Int32 > 0 {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Copyright</p>
|
|
<p style="color: var(--text-primary)">{ book.CopyrightYear.Int32 }</p>
|
|
</div>
|
|
}
|
|
if book.MangaType.Valid && book.MangaType.String != "" && book.MangaType.String != "unknown" {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Manga Type</p>
|
|
<p class="capitalize" style="color: var(--text-primary)">{ strings.ReplaceAll(book.MangaType.String, "_", " ") }</p>
|
|
</div>
|
|
}
|
|
if book.ScanInformation.Valid && book.ScanInformation.String != "" {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Scan Info</p>
|
|
<p class="text-sm" style="color: var(--text-secondary);">{ book.ScanInformation.String }</p>
|
|
</div>
|
|
}
|
|
if altSeries := getAlternateSeries(book.AlternateInfo); altSeries != "" {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Alternate Series</p>
|
|
<p style="color: var(--text-primary)">{ altSeries }</p>
|
|
</div>
|
|
}
|
|
if len(book.Contributors) > 0 {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Contributors</p>
|
|
<p style="color: var(--text-primary)">{ strings.Join(book.Contributors, ", ") }</p>
|
|
</div>
|
|
}
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Format</p>
|
|
<p style="color: var(--text-primary)">{ book.MimeType.String }</p>
|
|
</div>
|
|
if book.FileSize.Valid && book.FileSize.Int64 > 0 {
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">File Size</p>
|
|
<p style="color: var(--text-primary)">{ formatFileSize(book.FileSize.Int64) }</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
if book.GoodreadsID.Valid || book.OpenlibraryID.Valid || book.GoogleBooksID.Valid || book.Asin.Valid || book.Isbn.Valid || book.WebUrl.Valid {
|
|
<div class="mt-5 pt-4 border-t flex flex-wrap gap-2" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
|
if book.GoodreadsID.Valid && book.GoodreadsID.String != "" {
|
|
<a href={ getExternalURL("goodreads", book.GoodreadsID.String, book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Goodreads</a>
|
|
} else {
|
|
<a href={ getExternalURL("goodreads", "", book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Goodreads</a>
|
|
}
|
|
if book.OpenlibraryID.Valid && book.OpenlibraryID.String != "" {
|
|
<a href={ getExternalURL("openlibrary", book.OpenlibraryID.String, book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Open Library</a>
|
|
} else {
|
|
<a href={ getExternalURL("openlibrary", "", book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Open Library</a>
|
|
}
|
|
if book.GoogleBooksID.Valid && book.GoogleBooksID.String != "" {
|
|
<a href={ getExternalURL("googlebooks", book.GoogleBooksID.String, book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Google Books</a>
|
|
} else {
|
|
<a href={ getExternalURL("googlebooks", "", book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Google Books</a>
|
|
}
|
|
if book.Asin.Valid && book.Asin.String != "" {
|
|
<a href={ getExternalURL("amazon", book.Asin.String, book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Amazon</a>
|
|
} else if book.Isbn.Valid && book.Isbn.String != "" {
|
|
<a href={ getExternalURL("amazon", "", book.Isbn, book.Title, book.Author) } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">Amazon</a>
|
|
}
|
|
if book.WebUrl.Valid && book.WebUrl.String != "" {
|
|
<a href={ book.WebUrl.String } target="_blank" rel="noopener noreferrer" class="chip hover:opacity-90" style="background-color: var(--accent-muted); color: var(--accent); text-decoration: none;">{ getDomainName(book.WebUrl.String) }</a>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
<!-- Collections -->
|
|
if len(book.Collections) > 0 {
|
|
<div class="card p-6 mb-6">
|
|
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Collections</h3>
|
|
<div class="flex flex-wrap gap-2.5">
|
|
for _, col := range book.Collections {
|
|
<a
|
|
href={ "/collections/" + uuidToString(col.ID) }
|
|
class="inline-flex items-center gap-2 px-3 py-2 rounded-xl border hover:opacity-90 transition-opacity"
|
|
style={ "border-color: " + col.Color.String + "; background-color: var(--bg-primary); text-decoration: none;" }
|
|
>
|
|
<span style={ "color: " + col.Color.String }>{ col.Icon.String }</span>
|
|
<span style="color: var(--text-primary);">{ col.Name }</span>
|
|
</a>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
@ProgressSyncModal(user, book)
|
|
@NotesHighlightsModal(book)
|
|
@MetadataEditorModal(book)
|
|
@ErrorToast(errorMessage)
|
|
</body>
|
|
</html>
|
|
}
|