feat: implement comic/manga metadata display on book detail page
Implement comprehensive comic metadata display features on the SSR book detail page, supporting all 8 metadata fields from ComicInfo.xml and other sources. ## Template Changes (book_detail.templ) ### Step 1: Reading Direction Badge - Display directional badge (RTL, LTR, VERTICAL) for manga/comics - Uses 📖 icon with uppercase direction text - Auto-hides when direction is "auto" (default) - Styled with accent color for visibility ### Step 2: Community Rating Display - Show pre-existing community rating from metadata (0.0-10.0 scale) - Distinct from user ratings with visual differentiation - Uses renderStars() helper for visual star display - Shows both stars and numeric score (e.g., "★★★★☆ 8.5 / 10") - Smaller, subtler styling than user rating ### Step 3: Comic-Specific Badges - Age Rating: Content maturity indicator - Black & White: Visual style badge - Story Arc: Narrative arc name with 📚 icon - Badges styled as pills with subtle borders - Only display when values are present ### Step 4: Universal Series Info - Series Count: Total items in series - Volume: Volume/omnibus number - Imprint: Publisher imprint (e.g., Vertigo) ### Step 5: Comic-Specific Metadata - Manga Type: Raw/Comic/Manga classification - Scan Information: Scanner group, resolution - Alternate Series: Different series numbering ### Step 6: Summary Section - Display ComicInfo.xml summary when present - Separate from description field - Sanitized HTML output with bluemonday - Scrollable container for long summaries ### Step 7: Metadata Notes - Technical notes from metadata files - Internal/useful information (scanner, source, etc.) - Card-style display with clear typography ### Step 8: Web URL Link - External link to info sources (Goodreads, ComicVine, etc.) - Opens in new tab with security attributes - Displays clean domain name ## Utils Changes (templates/utils.go) Added helper functions: - getAlternateSeries(): Extract alternate series from JSONB - getDomainName(): Extract clean domain for display - formatAlternateInfo(): Format readable alternate series string ## Implementation Plan Updated FRONTEND_IMPLEMENTATION_PLAN_COMIC_METADATA.md with: - Disabled markdownlint for MD013 (line length) - Added spacing for readability ## Technical Details - All fields use pgtype.Text/Int4/Bool for NULL handling - Template conditionals check Valid flag before accessing values - Consistent styling using CSS custom properties - HTML escaping for security (except summary with bluemonday) - Responsive design with mobile-friendly layouts Related: Database schema already supports all metadata fields
This commit is contained in:
@@ -2,7 +2,9 @@ package templates
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -142,3 +144,56 @@ func getBookRating(rating *database.MediaRatings) int32 {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// getAlternateSeries extracts the alternate series name from JSONB data
|
||||
func getAlternateSeries(data []byte) string {
|
||||
if len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return ""
|
||||
}
|
||||
if series, ok := result["alternate_series"].(string); ok {
|
||||
return series
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getDomainName extracts a clean domain name from URL for display
|
||||
func getDomainName(rawURL string) string {
|
||||
if rawURL == "" {
|
||||
return "Source"
|
||||
}
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "Source"
|
||||
}
|
||||
// Return hostname without www.
|
||||
hostname := u.Hostname()
|
||||
return strings.TrimPrefix(hostname, "www.")
|
||||
}
|
||||
|
||||
// formatAlternateInfo formats alternate series info as readable string
|
||||
func formatAlternateInfo(data []byte) string {
|
||||
if len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var parts []string
|
||||
if series, ok := result["alternate_series"].(string); ok && series != "" {
|
||||
parts = append(parts, series)
|
||||
}
|
||||
if num, ok := result["alternate_number"].(float64); ok && num > 0 {
|
||||
parts = append(parts, fmt.Sprintf("#%d", int(num)))
|
||||
}
|
||||
if count, ok := result["alternate_count"].(float64); ok && count > 0 {
|
||||
parts = append(parts, fmt.Sprintf("(of %d)", int(count)))
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user