Improve star rating display on book detail page to show always-visible 5-star rating scale with theme-aware colors and visual half-star rendering. Changes: Enhanced renderStars() function: - Always displays 5 stars (0/5 now shows 5 grey stars instead of empty) - Filled stars use var(--accent) color (theme-aware highlight) - Empty stars use var(--text-secondary) (theme-aware grey, adapts to light/dark themes) - Half-stars use CSS linear-gradient (90deg) to split star vertically: - Left half: var(--accent) (filled, color) - Right half: var(--text-secondary) (empty, grey) - Uses webkit-background-clip and text-fill-color transparent for gradient effect Added getBookRating() helper function: - Returns rating value or 0 if book.Rating is nil - Allows unrated books to display 0/5 (5 grey stars) - Makes rating section always visible instead of hiding when nil Template changes: - Updated rating display to always show (no conditionals) - Removed text-yellow-400 class (colors now inline with theme vars) - Added templ.Raw() wrapper for HTML rendering (prevents escaping) - Simplified rating display logic Benefits: - Users can now see rating scale even when book isn't rated - Visual half-star is much more intuitive than ½ text character - Theme-aware colors adapt to light/dark mode automatically - Follows existing patterns ( UnsafeHTML, CSS variables, etc.) This makes the rating section more discoverable and user-friendly.
145 lines
3.6 KiB
Go
145 lines
3.6 KiB
Go
package templates
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
func activeClass(current, target string) string {
|
|
base := "block px-4 py-2 rounded-lg "
|
|
if current == target {
|
|
return base + "bg-accent text-bg-primary"
|
|
}
|
|
return base + "hover:opacity-80"
|
|
}
|
|
|
|
func ContainsString(slice []string, item string) bool {
|
|
for _, s := range slice {
|
|
if s == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func uuidToString(id pgtype.UUID) string {
|
|
if !id.Valid {
|
|
return ""
|
|
}
|
|
u, err := uuid.FromBytes(id.Bytes[0:16])
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return u.String()
|
|
}
|
|
|
|
// renderStars converts rating (1-10 scale) to star display
|
|
// Always displays 5 stars total with filled stars in yellow
|
|
// and empty stars in grey (using theme variable)
|
|
// Rating scale: 1-10 where each 2 points = 1 full star, odd numbers = half stars
|
|
func renderStars(rating int32) string {
|
|
const totalStars = 5
|
|
fullStars := rating / 2
|
|
hasHalf := rating%2 != 0
|
|
|
|
var stars strings.Builder
|
|
|
|
for i := int32(0); i < totalStars; i++ {
|
|
if i < fullStars {
|
|
// Filled star - accent color (theme-aware highlight)
|
|
stars.WriteString(`<span style="color: var(--accent);">★</span>`)
|
|
} else if i == fullStars && hasHalf {
|
|
// Half star - gradient from accent (left) to grey (right)
|
|
stars.WriteString(`<span style="background: linear-gradient(90deg, var(--accent) 50%, var(--text-secondary) 50%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">★</span>`)
|
|
} else {
|
|
// Empty star - grey using theme variable
|
|
stars.WriteString(`<span style="color: var(--text-secondary);">★</span>`)
|
|
}
|
|
}
|
|
|
|
return stars.String()
|
|
}
|
|
|
|
// formatFileSize converts bytes to human-readable format
|
|
func formatFileSize(bytes int64) string {
|
|
const (
|
|
KB = 1024
|
|
MB = KB * 1024
|
|
GB = MB * 1024
|
|
)
|
|
|
|
switch {
|
|
case bytes >= GB:
|
|
return fmt.Sprintf("%.2f GB", float64(bytes)/float64(GB))
|
|
case bytes >= MB:
|
|
return fmt.Sprintf("%.2f MB", float64(bytes)/float64(MB))
|
|
case bytes >= KB:
|
|
return fmt.Sprintf("%.2f KB", float64(bytes)/float64(KB))
|
|
default:
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
}
|
|
|
|
// getExternalURL generates URL for external book services
|
|
// Priority: ID > ISBN > Title+Author search
|
|
func getExternalURL(service string, id string, isbn pgtype.Text, title string, author pgtype.Text) string {
|
|
baseURL := ""
|
|
searchTerm := ""
|
|
|
|
// Determine search term: ID > ISBN > Title+Author
|
|
if id != "" {
|
|
searchTerm = id
|
|
} else if isbn.Valid && isbn.String != "" {
|
|
searchTerm = isbn.String
|
|
} else {
|
|
// Build title+author search query
|
|
if author.Valid && author.String != "" {
|
|
searchTerm = fmt.Sprintf("%s %s", title, author.String)
|
|
} else {
|
|
searchTerm = title
|
|
}
|
|
}
|
|
|
|
// Build URL based on service
|
|
switch service {
|
|
case "goodreads":
|
|
if id != "" {
|
|
baseURL = "https://www.goodreads.com/book/show/"
|
|
} else {
|
|
baseURL = "https://www.goodreads.com/search?q="
|
|
}
|
|
case "openlibrary":
|
|
if id != "" {
|
|
baseURL = "https://openlibrary.org/books/"
|
|
} else {
|
|
baseURL = "https://openlibrary.org/search?q="
|
|
}
|
|
case "googlebooks":
|
|
if id != "" {
|
|
baseURL = "https://books.google.com/books?id="
|
|
} else {
|
|
baseURL = "https://www.google.com/search?tbm=bks&q="
|
|
}
|
|
case "amazon":
|
|
// Amazon doesn't have direct book IDs, always search
|
|
baseURL = "https://www.amazon.com/s?k="
|
|
if isbn.Valid && isbn.String != "" {
|
|
searchTerm = isbn.String
|
|
}
|
|
}
|
|
|
|
return baseURL + searchTerm
|
|
}
|
|
|
|
// getBookRating returns the rating value or 0 if not rated
|
|
func getBookRating(rating *database.MediaRatings) int32 {
|
|
if rating != nil {
|
|
return rating.Rating
|
|
}
|
|
return 0
|
|
}
|