ui: update AnimeCard to support children and integrate across anime, notifications, and schedule templates

This commit is contained in:
2026-04-08 18:13:20 +02:00
parent b83f7f8ab3
commit 50aa32e51f
4 changed files with 93 additions and 68 deletions

View File

@@ -6,17 +6,57 @@ type AnimeCardProps struct {
ID int
Title string
ImageURL string
// Options to customize the card behavior
Class string // override default wrapper class
ImgClass string // override default image class
TitleClass string // override default title class
CurrentNode bool // if true, renders a div instead of an anchor tag (useful for graph nodes)
}
templ AnimeCard(props AnimeCardProps) {
<a href={ templ.URL(fmt.Sprintf("/anime/%d", props.ID)) }>
if props.ImageURL != "" {
<img src={ props.ImageURL } alt={ props.Title } class="catalog-thumb" loading="lazy"/>
} else {
<div class="no-image">No image</div>
}
</a>
<div class="catalog-title">
{ props.Title }
</div>
if props.CurrentNode {
<div class={ defaultString(props.Class, "catalog-item") }>
if props.ImageURL != "" {
<img src={ props.ImageURL } alt={ props.Title } class={ defaultString(props.ImgClass, "catalog-thumb") } loading="lazy"/>
} else {
<div class="no-image">No image</div>
}
<div class={ defaultString(props.TitleClass, "catalog-title") }>
{ props.Title }
</div>
{ children... }
</div>
} else {
<a href={ templ.URL(fmt.Sprintf("/anime/%d", props.ID)) } class={ props.Class }>
if props.Class == "notification-card" || props.Class == "schedule-card" {
<div class={ defaultString(props.ImgClass, "schedule-card-image") }>
if props.ImageURL != "" {
<img src={ props.ImageURL } alt={ props.Title } loading="lazy"/>
} else {
<div class="no-image">No image</div>
}
</div>
} else {
if props.ImageURL != "" {
<img src={ props.ImageURL } alt={ props.Title } class={ defaultString(props.ImgClass, "catalog-thumb") } loading="lazy"/>
} else {
<div class="no-image">No image</div>
}
}
if props.Class != "notification-card" && props.Class != "schedule-card" {
<div class={ defaultString(props.TitleClass, "catalog-title") }>
{ props.Title }
</div>
}
{ children... }
</a>
}
}
func defaultString(val, fallback string) string {
if val == "" {
return fallback
}
return val
}

View File

@@ -282,25 +282,15 @@ templ AnimeRelationsList(relations []jikan.RelationEntry) {
if len(relations) > 1 {
<div class="relations-grid">
for _, rel := range relations {
if rel.IsCurrent {
<div class="relation-card current">
if rel.Anime.ImageURL() != "" {
<img src={ rel.Anime.ImageURL() } alt={ rel.Anime.DisplayTitle() } class="relation-thumb"/>
} else {
<div class="no-image">No image</div>
}
<div class="relation-title">{ rel.Anime.DisplayTitle() }</div>
</div>
} else {
<a href={ templ.URL(fmt.Sprintf("/anime/%d", rel.Anime.MalID)) } class="relation-card">
if rel.Anime.ImageURL() != "" {
<img src={ rel.Anime.ImageURL() } alt={ rel.Anime.DisplayTitle() } class="relation-thumb"/>
} else {
<div class="no-image">No image</div>
}
<div class="relation-title">{ rel.Anime.DisplayTitle() }</div>
</a>
}
@ui.AnimeCard(ui.AnimeCardProps{
ID: rel.Anime.MalID,
Title: rel.Anime.DisplayTitle(),
ImageURL: rel.Anime.ImageURL(),
Class: "relation-card" + func() string { if rel.IsCurrent { return " current" }; return "" }(),
ImgClass: "relation-thumb",
TitleClass: "relation-title",
CurrentNode: rel.IsCurrent,
})
}
</div>
} else {
@@ -312,14 +302,14 @@ templ AnimeRecommendations(recs []jikan.Anime) {
if len(recs) > 0 {
<div class="relations-grid">
for _, anime := range recs {
<a href={ templ.URL(fmt.Sprintf("/anime/%d", anime.MalID)) } class="relation-card">
if anime.ImageURL() != "" {
<img src={ anime.ImageURL() } alt={ anime.DisplayTitle() } class="relation-thumb"/>
} else {
<div class="no-image">No image</div>
}
<div class="relation-title">{ anime.DisplayTitle() }</div>
</a>
@ui.AnimeCard(ui.AnimeCardProps{
ID: anime.MalID,
Title: anime.DisplayTitle(),
ImageURL: anime.ImageURL(),
Class: "relation-card",
ImgClass: "relation-thumb",
TitleClass: "relation-title",
})
}
</div>
} else {

View File

@@ -2,6 +2,7 @@ package templates
import "mal/internal/jikan"
import "mal/internal/database"
import "mal/internal/shared/ui"
import "fmt"
type WatchingAnimeWithDetails struct {
@@ -32,10 +33,7 @@ templ Notifications(watching []WatchingAnimeWithDetails) {
<p class="notifications-subtitle">Because you've watched prequels.</p>
<div hx-get="/notifications/upcoming" hx-trigger="load, every 15s" hx-swap="innerHTML">
<div class="loading-indicator">
<div class="loading-dot"></div><div class="loading-dot"></div><div class="loading-dot"></div>
<span>Syncing sequel graphs...</span>
</div>
@ui.LoadingIndicator("Syncing sequel graphs...")
</div>
</div>
}
@@ -86,14 +84,13 @@ templ renderSplitSeasons(upcomingSeasons []database.GetUpcomingSeasonsRow) {
}
templ UpcomingSeasonCard(item database.GetUpcomingSeasonsRow) {
<a href={ templ.URL(fmt.Sprintf("/anime/%d", item.ID)) } class="notification-card">
<div class="notification-image">
if item.ImageUrl != "" {
<img src={ item.ImageUrl } alt={ displaySeasonTitle(item) } loading="lazy"/>
} else {
<div class="no-image">No image</div>
}
</div>
@ui.AnimeCard(ui.AnimeCardProps{
ID: int(item.ID),
Title: displaySeasonTitle(item),
ImageURL: item.ImageUrl,
Class: "notification-card",
ImgClass: "notification-image",
}) {
<div class="notification-content">
<div class="notification-title">
{ displaySeasonTitle(item) }
@@ -102,7 +99,7 @@ templ UpcomingSeasonCard(item database.GetUpcomingSeasonsRow) {
<span class="notification-broadcast" style="color: var(--text-muted) !important;">Because you watched { item.PrequelTitle }</span>
</div>
</div>
</a>
}
}
func displaySeasonTitle(entry database.GetUpcomingSeasonsRow) string {
@@ -110,14 +107,13 @@ func displaySeasonTitle(entry database.GetUpcomingSeasonsRow) string {
}
templ NotificationCard(item WatchingAnimeWithDetails) {
<a href={ templ.URL(fmt.Sprintf("/anime/%d", item.Entry.AnimeID)) } class="notification-card">
<div class="notification-image">
if item.Entry.ImageUrl != "" {
<img src={ item.Entry.ImageUrl } alt={ displayTitle(item.Entry) } loading="lazy"/>
} else {
<div class="no-image">No image</div>
}
</div>
@ui.AnimeCard(ui.AnimeCardProps{
ID: int(item.Entry.AnimeID),
Title: displayTitle(item.Entry),
ImageURL: item.Entry.ImageUrl,
Class: "notification-card",
ImgClass: "notification-image",
}) {
<div class="notification-content">
<div class="notification-title">
{ displayTitle(item.Entry) }
@@ -141,7 +137,7 @@ templ NotificationCard(item WatchingAnimeWithDetails) {
}
</div>
</div>
</a>
}
}
func displayTitle(entry database.GetWatchingAnimeRow) string {

View File

@@ -52,14 +52,13 @@ templ ScheduleDay(day string, animes []jikan.Anime) {
}
templ ScheduleAnimeCard(anime jikan.Anime) {
<a href={ templ.URL(fmt.Sprintf("/anime/%d", anime.MalID)) } class="schedule-card">
<div class="schedule-card-image">
if anime.ImageURL() != "" {
<img src={ anime.ImageURL() } alt={ anime.DisplayTitle() } loading="lazy"/>
} else {
<div class="no-image">No image</div>
}
</div>
@ui.AnimeCard(ui.AnimeCardProps{
ID: anime.MalID,
Title: anime.DisplayTitle(),
ImageURL: anime.ImageURL(),
Class: "schedule-card",
ImgClass: "schedule-card-image",
}) {
<div class="schedule-card-info">
<div class="schedule-card-title">{ anime.DisplayTitle() }</div>
<div class="schedule-card-meta">
@@ -77,5 +76,5 @@ templ ScheduleAnimeCard(anime jikan.Anime) {
<div class="schedule-card-score">★ { fmt.Sprintf("%.1f", anime.Score) }</div>
}
</div>
</a>
}
}