docs: add frontend implementation plan for comic metadata fields
Add comprehensive planning document for displaying new ComicInfo.xml metadata fields on the book detail page. New fields to be added: - Reading direction (RTL/LTR/vertical) badge - Community rating display (0-10 scale) - Universal fields: series count, volume, imprint, age rating - Comic-specific: manga type, story arc, scan info, B&W flag Implementation approach: - SSR-first rendering (no client-side fetching) - TailwindCSS only (no custom CSS) - Conditional display based on field validity - Follows existing template patterns This document provides step-by-step implementation guidance with code examples and testing scenarios for the frontend team. Related: IMPLEMENTATION_PLAN_MERGE_METADATA_READING_DIRECTION.md
This commit is contained in:
@@ -0,0 +1,510 @@
|
|||||||
|
# Frontend Implementation Plan: Add Comic Metadata Fields to Book Detail Page
|
||||||
|
|
||||||
|
**Status:** Planning Document (Not Implemented)
|
||||||
|
**Date:** March 30, 2026
|
||||||
|
**Scope:** Display new comic/manga metadata fields on the book detail page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines how to add the new ComicInfo.xml metadata fields to the book detail page frontend. These fields are already added to the database schema and backend models (per `IMPLEMENTATION_PLAN_MERGE_METADATA_READING_DIRECTION.md`).
|
||||||
|
|
||||||
|
## New Fields to Display
|
||||||
|
|
||||||
|
### Universal Fields (apply to all formats: ebooks, audiobooks, comics)
|
||||||
|
- `series_count` - Total items in series
|
||||||
|
- `volume` - Volume/omnibus number
|
||||||
|
- `imprint` - Publisher imprint (e.g., Vertigo, HarperCollinsEpic)
|
||||||
|
- `age_rating` - Age rating: Everyone, Teen, Mature, Adult
|
||||||
|
- `web_url` - URL to info page (Goodreads, ComicVine, MangaUpdates, etc.)
|
||||||
|
- `metadata_notes` - Notes from metadata files (ComicInfo.xml, EPUB, PDF)
|
||||||
|
- `community_rating` - Pre-existing community rating (0.0-10.0) - **distinct from user ratings**
|
||||||
|
|
||||||
|
### Comic-Specific Fields
|
||||||
|
- `manga_type` - Raw Manga field: unknown, no, yes, yes_and_right_to_left
|
||||||
|
- `reading_direction` - Computed reading direction: auto, ltr, rtl, vertical
|
||||||
|
- `story_arc` - Story arc name (e.g., "The Dark Phoenix Saga", "Civil War")
|
||||||
|
- `is_black_and_white` - Black and white flag
|
||||||
|
- `alternate_info` - JSONB: {alternate_series, alternate_number, alternate_count}
|
||||||
|
- `scan_information` - Scan information (scanner group, resolution, etc.)
|
||||||
|
- `summary` - Summary from ComicInfo.xml (may merge with description)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Book Detail Page Structure
|
||||||
|
|
||||||
|
**File:** `templates/book_detail.templ`
|
||||||
|
|
||||||
|
### Current Layout Sections:
|
||||||
|
|
||||||
|
1. **Top Section** (lines 23-130)
|
||||||
|
- Cover image (left)
|
||||||
|
- Title, author, action buttons (right)
|
||||||
|
- Rating display (user ratings)
|
||||||
|
- Series badge
|
||||||
|
- Description/Synopsis
|
||||||
|
|
||||||
|
2. **Progress Section** (lines 132-185)
|
||||||
|
- Reading progress bar
|
||||||
|
- Progress stats grid
|
||||||
|
|
||||||
|
3. **Metadata Grid** (lines 186-253)
|
||||||
|
- Publication info (Publisher, Published, ISBN, Language, Edition, Pages, Genre, Copyright Year)
|
||||||
|
- Technical info (Format, File Size)
|
||||||
|
- External links section
|
||||||
|
|
||||||
|
4. **Collections Section** (lines 347-367)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Step 1: Add Reading Direction Badge (Display Priority: HIGH)
|
||||||
|
|
||||||
|
**Location:** After Series Badge (line 117)
|
||||||
|
|
||||||
|
**Rationale:** Reading direction is critical for manga/comic readers. Display prominently like the series badge.
|
||||||
|
|
||||||
|
```templ
|
||||||
|
<!-- Reading Direction Badge (for manga/comics) -->
|
||||||
|
if book.ReadingDirection.Valid && book.ReadingDirection.String != "" && book.ReadingDirection.String != "auto" {
|
||||||
|
<div class="mb-4">
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full text-sm font-semibold"
|
||||||
|
style="background-color: var(--accent); color: white;"
|
||||||
|
>
|
||||||
|
📖 { strings.ToUpper(book.ReadingDirection.String) }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Styling Notes:**
|
||||||
|
- Use `var(--accent)` for consistency with series badge
|
||||||
|
- Only show if not "auto" (i.e., explicitly set to ltr, rtl, or vertical)
|
||||||
|
- Icon: 📖 for visual clarity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: Add Community Rating Display (Display Priority: HIGH)
|
||||||
|
|
||||||
|
**Location:** After User Rating Display (line 103)
|
||||||
|
|
||||||
|
**Rationale:** Community rating complements user rating. Show side-by-side for comparison.
|
||||||
|
|
||||||
|
```templ
|
||||||
|
<!-- Community Rating Display (from metadata) -->
|
||||||
|
if book.CommunityRating.Valid && book.CommunityRating.Float64 > 0 {
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="text-lg" style="color: var(--text-secondary);">
|
||||||
|
Community Rating:
|
||||||
|
<span class="font-bold" style="color: var(--text-primary);">
|
||||||
|
@templ.Raw(renderStars(getBookRating(int(book.CommunityRating.Float64 * 2))))
|
||||||
|
</span>
|
||||||
|
<span class="ml-2 text-sm" style="color: var(--text-secondary);">
|
||||||
|
({ fmt.Sprintf("%.1f", book.CommunityRating.Float64) } / 10)
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Styling Notes:**
|
||||||
|
- Smaller text than user rating (text-lg vs text-2xl)
|
||||||
|
- Gray/subtle color to distinguish from user rating
|
||||||
|
- Scale: Community rating is 0-10, user rating display is 0-5 stars
|
||||||
|
- Conversion: `community_rating * 2` to get 0-10 scale for star display
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: Add Comic-Specific Badges (Display Priority: MEDIUM)
|
||||||
|
|
||||||
|
**Location:** After Reading Direction Badge (line 117)
|
||||||
|
|
||||||
|
**Rationale:** Group all manga/comic-specific badges together for easy scanning.
|
||||||
|
|
||||||
|
```templ
|
||||||
|
<!-- Comic-Specific Badges -->
|
||||||
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
|
<!-- Age Rating Badge -->
|
||||||
|
if book.AgeRating.Valid && book.AgeRating.String != "" {
|
||||||
|
<span
|
||||||
|
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||||
|
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
|
||||||
|
>
|
||||||
|
{ book.AgeRating.String }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Black and White Badge -->
|
||||||
|
if book.IsBlackAndWhite.Valid && book.IsBlackAndWhite.Bool {
|
||||||
|
<span
|
||||||
|
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||||
|
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
|
||||||
|
>
|
||||||
|
B&W
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Story Arc Badge -->
|
||||||
|
if book.StoryArc.Valid && book.StoryArc.String != "" {
|
||||||
|
<span
|
||||||
|
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||||
|
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
|
||||||
|
>
|
||||||
|
📚 { book.StoryArc.String }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Styling Notes:**
|
||||||
|
- Use subtle styling (smaller, outlined) to differentiate from primary badges
|
||||||
|
- Flex wrap for responsive layout
|
||||||
|
- Icons: 📚 for story arc
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4: Add Universal Series Info to Metadata Grid (Display Priority: HIGH)
|
||||||
|
|
||||||
|
**Location:** In Metadata Grid section, after Series/Number fields
|
||||||
|
|
||||||
|
**Note:** The current template doesn't display Series/Number in the metadata grid (only as badge). We'll add all series-related fields together.
|
||||||
|
|
||||||
|
**Insertion Point:** After Genre field (line 235)
|
||||||
|
|
||||||
|
```templ
|
||||||
|
<!-- Series Information (if not shown in badge) -->
|
||||||
|
if book.SeriesCount.Valid && book.SeriesCount.Int32 > 0 {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Series Count</p>
|
||||||
|
<p>{ book.SeriesCount.Int32 } items</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if book.Volume.Valid && book.Volume.Int32 > 0 {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Volume</p>
|
||||||
|
<p>Vol. { book.Volume.Int32 }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if book.Imprint.Valid && book.Imprint.String != "" {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Imprint</p>
|
||||||
|
<p>{ book.Imprint.String }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 5: Add Comic-Specific Metadata to Grid (Display Priority: MEDIUM)
|
||||||
|
|
||||||
|
**Location:** In Metadata Grid section, after Copyright Year (line 241)
|
||||||
|
|
||||||
|
```templ
|
||||||
|
<!-- Comic-Specific Fields -->
|
||||||
|
if book.MangaType.Valid && book.MangaType.String != "" && book.MangaType.String != "unknown" {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Manga Type</p>
|
||||||
|
<p class="capitalize">{ strings.ReplaceAll(book.MangaType.String, "_", " ") }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if book.ScanInformation.Valid && book.ScanInformation.String != "" {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Scan Info</p>
|
||||||
|
<p class="text-sm" style="color: var(--text-secondary);">{ book.ScanInformation.String }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if book.AlternateInfo.Valid && len(book.AlternateInfo.Bytes) > 0 {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Alternate Series</p>
|
||||||
|
<p class="text-sm">{ string(book.AlternateInfo.Bytes) }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note for AlternateInfo:** This is JSONB data stored as `[]byte`. You may want to:
|
||||||
|
1. Parse the JSON and display formatted
|
||||||
|
2. Or create a helper function to extract specific fields
|
||||||
|
|
||||||
|
Helper function example (add to `templates/helpers.go`):
|
||||||
|
|
||||||
|
```go
|
||||||
|
// getAlternateSeries extracts the alternate series name from JSONB data
|
||||||
|
func getAlternateSeries(data pgtype.Bytes) string {
|
||||||
|
if !data.Valid || len(data.Bytes) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data.Bytes, &result); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if series, ok := result["alternate_series"].(string); ok {
|
||||||
|
return series
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in template:
|
||||||
|
```templ
|
||||||
|
if altSeries := getAlternateSeries(book.AlternateInfo); altSeries != "" {
|
||||||
|
<div>
|
||||||
|
<p style="color: var(--text-secondary)">Alternate Series</p>
|
||||||
|
<p>{ altSeries }</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 6: Add Summary Section (Display Priority: MEDIUM)
|
||||||
|
|
||||||
|
**Location:** After Description/Synopsis section (line 128)
|
||||||
|
|
||||||
|
**Rationale:** Summary from ComicInfo.xml may differ from Calibre description. Show both if present.
|
||||||
|
|
||||||
|
```templ
|
||||||
|
<!-- Summary (from ComicInfo.xml, if different from description) -->
|
||||||
|
if book.Summary.Valid && book.Summary.String != "" && book.Summary.String != book.Description.String {
|
||||||
|
<div class="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" style="color: var(--text-secondary)">
|
||||||
|
@UnsafeHTML(
|
||||||
|
bluemonday.UGCPolicy().Sanitize(book.Summary.String),
|
||||||
|
).ToComponent()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 7: Add Metadata Notes Section (Display Priority: LOW)
|
||||||
|
|
||||||
|
**Location:** After Summary section (or at the bottom before Collections)
|
||||||
|
|
||||||
|
**Rationale:** Metadata notes are technical/scanner information. Less important for end users.
|
||||||
|
|
||||||
|
```templ
|
||||||
|
<!-- Metadata Notes (technical notes from metadata files) -->
|
||||||
|
if book.MetadataNotes.Valid && book.MetadataNotes.String != "" {
|
||||||
|
<div
|
||||||
|
class="card p-6 rounded-lg border mb-6"
|
||||||
|
style="background-color: var(--bg-secondary); border-color: var(--border);"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 8: Add Web URL Link (Display Priority: MEDIUM)
|
||||||
|
|
||||||
|
**Location:** In External Links section (line 258)
|
||||||
|
|
||||||
|
**Rationale:** Add web_url as another external link alongside Goodreads, OpenLibrary, etc.
|
||||||
|
|
||||||
|
```templ
|
||||||
|
<!-- In the External Links flex container -->
|
||||||
|
if book.WebUrl.Valid && book.WebUrl.String != "" {
|
||||||
|
<a
|
||||||
|
href={ book.WebUrl.String }
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-sm hover:underline flex items-center gap-1"
|
||||||
|
style="color: var(--accent);"
|
||||||
|
>
|
||||||
|
🔗 { getDomainName(book.WebUrl.String) }
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Helper function to extract domain name (add to `templates/helpers.go`):
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 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.")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TypeScript Updates (No Changes Required)
|
||||||
|
|
||||||
|
**File:** `web/src/book-detail.ts`
|
||||||
|
|
||||||
|
**Status:** No changes needed. This file only handles Alpine.js modal visibility functions.
|
||||||
|
|
||||||
|
**Note:** The book detail page uses SSR (server-side rendering) exclusively. No client-side data fetching or Alpine.js data manipulation is needed for these new fields.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template Helper Functions Needed
|
||||||
|
|
||||||
|
Add these functions to `templates/helpers.go` (or existing helpers file):
|
||||||
|
|
||||||
|
```go
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getAlternateSeries extracts the alternate series name from JSONB data
|
||||||
|
func getAlternateSeries(data pgtype.Bytes) string {
|
||||||
|
if !data.Valid || len(data.Bytes) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data.Bytes, &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"
|
||||||
|
}
|
||||||
|
hostname := u.Hostname()
|
||||||
|
return strings.TrimPrefix(hostname, "www.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatAlternateInfo formats alternate series info as readable string
|
||||||
|
func formatAlternateInfo(data pgtype.Bytes) string {
|
||||||
|
if !data.Valid || len(data.Bytes) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data.Bytes, &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, " ")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Add Reading Direction Badge (after line 117)
|
||||||
|
- [ ] **Step 2:** Add Community Rating Display (after line 103)
|
||||||
|
- [ ] **Step 3:** Add Comic-Specific Badges (after line 117)
|
||||||
|
- [ ] **Step 4:** Add Universal Series Info to Metadata Grid (after line 235)
|
||||||
|
- [ ] **Step 5:** Add Comic-Specific Metadata to Grid (after line 241)
|
||||||
|
- [ ] **Step 6:** Add Summary Section (after line 128)
|
||||||
|
- [ ] **Step 7:** Add Metadata Notes Section (before Collections)
|
||||||
|
- [ ] **Step 8:** Add Web URL Link (in External Links section)
|
||||||
|
- [ ] Add template helper functions to `templates/helpers.go`
|
||||||
|
- [ ] Test with manga book (RTL reading direction)
|
||||||
|
- [ ] Test with Western comic (LTR reading direction)
|
||||||
|
- [ ] Test with webtoon (vertical reading direction)
|
||||||
|
- [ ] Test with ebook (no comic metadata)
|
||||||
|
- [ ] Verify responsive layout on mobile/desktop
|
||||||
|
- [ ] Verify dark/light theme compatibility
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
### Test Case 1: Japanese Manga
|
||||||
|
**Expected Display:**
|
||||||
|
- Reading Direction: "RTL" badge
|
||||||
|
- Manga Type: "yes_and_right_to_left" in metadata grid
|
||||||
|
- Community Rating: displayed alongside user rating
|
||||||
|
|
||||||
|
### Test Case 2: Western Comic
|
||||||
|
**Expected Display:**
|
||||||
|
- Reading Direction: "LTR" badge (or hidden if auto)
|
||||||
|
- Age Rating: "Teen" or "Mature" badge
|
||||||
|
- Story Arc: displayed if present
|
||||||
|
- Publisher/Imprint: displayed in metadata grid
|
||||||
|
|
||||||
|
### Test Case 3: Webtoon/Manhwa
|
||||||
|
**Expected Display:**
|
||||||
|
- Reading Direction: "vertical" badge
|
||||||
|
- Language: "ko" (Korean) in metadata grid
|
||||||
|
- Format-specific metadata displayed
|
||||||
|
|
||||||
|
### Test Case 4: Regular Ebook
|
||||||
|
**Expected Display:**
|
||||||
|
- No reading direction badge (or "auto" hidden)
|
||||||
|
- No comic-specific fields
|
||||||
|
- Only universal fields (series count, volume, imprint, age rating) if present
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Principles Followed
|
||||||
|
|
||||||
|
1. **✅ SSR-First:** All data server-side rendered, no client-side fetching
|
||||||
|
2. **✅ Progressive Enhancement:** Page works without JavaScript
|
||||||
|
3. **✅ TailwindCSS Only:** No custom CSS added
|
||||||
|
4. **✅ Existing Patterns:** Follows current badge/grid styling
|
||||||
|
5. **✅ Responsive:** Uses existing responsive grid classes
|
||||||
|
6. **✅ Theme-Aware:** Uses CSS variables (`var(--accent)`, `var(--text-secondary)`, etc.)
|
||||||
|
7. **✅ Accessible:** Semantic HTML, proper contrast
|
||||||
|
8. **✅ Conditional Rendering:** Only show fields if they have values
|
||||||
|
9. **✅ No Backend Changes:** Frontend-only, uses existing API/handler types
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- `IMPLEMENTATION_PLAN_MERGE_METADATA_READING_DIRECTION.md` - Full backend implementation
|
||||||
|
- `PROJECT_GUIDELINES.md` - Project coding standards and conventions
|
||||||
|
- `templates/book_detail.templ` - Current book detail template
|
||||||
|
- `internal/handlers/media_detail.go` - MediaDetail struct definition
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **No TypeScript changes needed:** All data is SSR'd from Go handlers
|
||||||
|
- **No API changes needed:** MediaDetail already embeds database.MediaItems with new fields
|
||||||
|
- **Template helpers:** May need helper functions for JSONB parsing and URL display
|
||||||
|
- **Styling consistency:** All new elements use existing CSS variables and Tailwind classes
|
||||||
|
- **Conditional display:** Most fields should only display if they have values (check `.Valid`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**End of Document**
|
||||||
Reference in New Issue
Block a user