311 lines
10 KiB
Plaintext
311 lines
10 KiB
Plaintext
package templates
|
|
|
|
import "mal/internal/jikan"
|
|
import "mal/internal/shared/ui"
|
|
import "fmt"
|
|
import "strings"
|
|
|
|
templ AnimeDetails(anime jikan.Anime, currentStatus string) {
|
|
@Layout("mal - " + anime.DisplayTitle()) {
|
|
<div class="anime-page">
|
|
<div class="anime-main">
|
|
<div class="anime-hero anime-surface">
|
|
<div class="anime-poster">
|
|
if anime.ImageURL() != "" {
|
|
<img src={ anime.ImageURL() } alt={ anime.DisplayTitle() }/>
|
|
} else {
|
|
<div class="no-image">No image</div>
|
|
}
|
|
</div>
|
|
<div class="anime-info">
|
|
<h1>{ anime.DisplayTitle() }</h1>
|
|
if anime.TitleJapanese != "" {
|
|
<p class="anime-alt-title">{ anime.TitleJapanese }</p>
|
|
}
|
|
<div class="anime-quick-info">
|
|
if anime.ShortRating() != "" {
|
|
<span class="info-tag">{ anime.ShortRating() }</span>
|
|
}
|
|
if anime.Type != "" {
|
|
<span class="info-tag">{ anime.Type }</span>
|
|
}
|
|
if anime.Episodes > 0 {
|
|
<span class="info-tag">{ fmt.Sprintf("%d ep", anime.Episodes) }</span>
|
|
}
|
|
if anime.ShortDuration() != "" {
|
|
<span class="info-tag">{ anime.ShortDuration() }</span>
|
|
}
|
|
</div>
|
|
<div class="anime-actions">
|
|
@WatchlistDropdown(anime.MalID, anime.Title, anime.TitleEnglish, anime.TitleJapanese, anime.ImageURL(), currentStatus, anime.Airing)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<section class="anime-synopsis anime-surface anime-section">
|
|
<h3>Synopsis</h3>
|
|
if anime.Synopsis != "" {
|
|
<p>{ anime.Synopsis }</p>
|
|
} else {
|
|
<p class="no-synopsis">No synopsis available.</p>
|
|
}
|
|
</section>
|
|
<section class="anime-relations anime-surface anime-section">
|
|
<h3>Related</h3>
|
|
<div hx-get={ string(templ.URL(fmt.Sprintf("/api/anime/%d/relations", anime.MalID))) } hx-trigger="load">
|
|
@ui.LoadingIndicator("Loading relations")
|
|
</div>
|
|
</section>
|
|
<section class="anime-recommendations anime-surface anime-section">
|
|
<h3>Recommendations</h3>
|
|
<div hx-get={ string(templ.URL(fmt.Sprintf("/api/anime/%d/recommendations", anime.MalID))) } hx-trigger="load">
|
|
@ui.LoadingIndicator("Loading recommendations")
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<aside class="anime-sidebar anime-surface">
|
|
<div class="anime-side-section">
|
|
<h3>Details</h3>
|
|
if anime.Aired.String != "" {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">Aired</span>
|
|
<span class="sidebar-value">{ anime.Aired.String }</span>
|
|
</div>
|
|
}
|
|
if anime.Premiered() != "" {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">Premiered</span>
|
|
<span class="sidebar-value">{ anime.Premiered() }</span>
|
|
</div>
|
|
}
|
|
if anime.Status != "" {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">Status</span>
|
|
<span class="sidebar-value">{ anime.Status }</span>
|
|
</div>
|
|
}
|
|
if anime.Score > 0 {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">MAL Score</span>
|
|
<span class="sidebar-value">{ fmt.Sprintf("%.2f", anime.Score) }</span>
|
|
</div>
|
|
}
|
|
if anime.Duration != "" {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">Duration</span>
|
|
<span class="sidebar-value">{ anime.Duration }</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
if len(anime.Genres) > 0 {
|
|
<div class="anime-side-section">
|
|
<h3>Genres</h3>
|
|
<div class="sidebar-tags">
|
|
for _, g := range anime.Genres {
|
|
<span class="sidebar-tag">{ g.Name }</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
if hasExtraSidebarDetails(anime) {
|
|
<details class="anime-side-section side-details-more">
|
|
<summary>More metadata</summary>
|
|
if anime.TitleJapanese != "" {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">Japanese</span>
|
|
<span class="sidebar-value">{ anime.TitleJapanese }</span>
|
|
</div>
|
|
}
|
|
if len(anime.TitleSynonyms) > 0 {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">Synonyms</span>
|
|
<span class="sidebar-value">{ strings.Join(anime.TitleSynonyms, ", ") }</span>
|
|
</div>
|
|
}
|
|
if len(anime.Studios) > 0 {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">Studios</span>
|
|
<span class="sidebar-value">{ joinNames(anime.Studios) }</span>
|
|
</div>
|
|
}
|
|
if len(anime.Producers) > 0 {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">Producers</span>
|
|
<span class="sidebar-value">{ joinNames(anime.Producers) }</span>
|
|
</div>
|
|
}
|
|
if anime.Source != "" {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">Source</span>
|
|
<span class="sidebar-value">{ anime.Source }</span>
|
|
</div>
|
|
}
|
|
if len(anime.Demographics) > 0 {
|
|
<div class="sidebar-row sidebar-row-wrap">
|
|
<span class="sidebar-label">Demographics</span>
|
|
<div class="sidebar-tags">
|
|
for _, d := range anime.Demographics {
|
|
<span class="sidebar-tag">{ d.Name }</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
if len(anime.Themes) > 0 {
|
|
<div class="sidebar-row sidebar-row-wrap">
|
|
<span class="sidebar-label">Themes</span>
|
|
<div class="sidebar-tags">
|
|
for _, t := range anime.Themes {
|
|
<span class="sidebar-tag">{ t.Name }</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
if anime.Broadcast.String != "" {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">Broadcast</span>
|
|
<span class="sidebar-value">{ anime.Broadcast.String }</span>
|
|
</div>
|
|
}
|
|
if len(anime.Streaming) > 0 {
|
|
<div class="sidebar-row">
|
|
<span class="sidebar-label">Streaming</span>
|
|
<div class="sidebar-value">
|
|
for _, s := range anime.Streaming {
|
|
<div><a href={ templ.URL(s.URL) } target="_blank">{ s.Name }</a></div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</details>
|
|
}
|
|
</aside>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
func joinNames(entities []jikan.NamedEntity) string {
|
|
names := make([]string, len(entities))
|
|
for i, e := range entities {
|
|
names[i] = e.Name
|
|
}
|
|
return strings.Join(names, ", ")
|
|
}
|
|
|
|
templ WatchlistDropdown(animeID int, animeTitle string, animeTitleEnglish string, animeTitleJapanese string, animeImage string, currentStatus string, airing bool) {
|
|
<div class="dropdown" id="watchlist-dropdown">
|
|
<button class="dropdown-trigger" onclick="toggleDropdown()">
|
|
if currentStatus != "" {
|
|
{ formatStatus(currentStatus) }
|
|
} else {
|
|
add to watchlist
|
|
}
|
|
<span class="dropdown-arrow">▾</span>
|
|
</button>
|
|
<div class="dropdown-menu">
|
|
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "watching", currentStatus, airing)
|
|
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "completed", currentStatus, airing)
|
|
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "on_hold", currentStatus, airing)
|
|
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "dropped", currentStatus, airing)
|
|
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "plan_to_watch", currentStatus, airing)
|
|
if currentStatus != "" {
|
|
<div class="dropdown-divider"></div>
|
|
<button
|
|
class="dropdown-item remove"
|
|
hx-delete={ string(templ.URL(fmt.Sprintf("/api/watchlist/%d", animeID))) }
|
|
hx-target="#watchlist-dropdown"
|
|
hx-swap="outerHTML"
|
|
>Remove from list</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ dropdownStatusOption(animeID int, animeTitle string, animeTitleEnglish string, animeTitleJapanese string, animeImage string, status string, currentStatus string, airing bool) {
|
|
<button
|
|
class={ "dropdown-item", templ.KV("active", status == currentStatus) }
|
|
hx-post="/api/watchlist"
|
|
hx-vals={ fmt.Sprintf(`{"anime_id": "%d", "anime_title": "%s", "anime_title_english": "%s", "anime_title_japanese": "%s", "anime_image": "%s", "status": "%s", "airing": "%v"}`, animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, status, airing) }
|
|
hx-target="#watchlist-dropdown"
|
|
hx-swap="outerHTML"
|
|
>
|
|
{ formatStatus(status) }
|
|
if status == currentStatus {
|
|
<span class="check">✓</span>
|
|
}
|
|
</button>
|
|
}
|
|
|
|
templ statusOption(anime jikan.Anime, status string, currentStatus string) {
|
|
<button
|
|
class={ "dropdown-item", templ.KV("active", status == currentStatus) }
|
|
hx-post="/api/watchlist"
|
|
hx-vals={ fmt.Sprintf(`{"anime_id": "%d", "anime_title": "%s", "anime_title_english": "%s", "anime_title_japanese": "%s", "anime_image": "%s", "status": "%s"}`, anime.MalID, anime.Title, anime.TitleEnglish, anime.TitleJapanese, anime.ImageURL(), status) }
|
|
hx-target="#watchlist-dropdown"
|
|
hx-swap="outerHTML"
|
|
>
|
|
{ formatStatus(status) }
|
|
if status == currentStatus {
|
|
<span class="check">✓</span>
|
|
}
|
|
</button>
|
|
}
|
|
|
|
func formatStatus(status string) string {
|
|
switch status {
|
|
case "watching":
|
|
return "Watching"
|
|
case "completed":
|
|
return "Completed"
|
|
case "on_hold":
|
|
return "On hold"
|
|
case "dropped":
|
|
return "Dropped"
|
|
case "plan_to_watch":
|
|
return "Plan to watch"
|
|
default:
|
|
return status
|
|
}
|
|
}
|
|
|
|
templ AnimeRelationsList(relations []jikan.RelationEntry) {
|
|
if len(relations) > 1 {
|
|
<div class="relations-grid">
|
|
for _, rel := range relations {
|
|
@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 {
|
|
<p class="empty-inline-note">No related anime found.</p>
|
|
}
|
|
}
|
|
|
|
templ AnimeRecommendations(recs []jikan.Anime) {
|
|
if len(recs) > 0 {
|
|
<div class="relations-grid">
|
|
for _, anime := range recs {
|
|
@ui.AnimeCard(ui.AnimeCardProps{
|
|
ID: anime.MalID,
|
|
Title: anime.DisplayTitle(),
|
|
ImageURL: anime.ImageURL(),
|
|
Class: "relation-card",
|
|
ImgClass: "relation-thumb",
|
|
TitleClass: "relation-title",
|
|
})
|
|
}
|
|
</div>
|
|
} else {
|
|
<p class="empty-inline-note">No recommendations available.</p>
|
|
}
|
|
}
|
|
|
|
func hasExtraSidebarDetails(anime jikan.Anime) bool {
|
|
return anime.TitleJapanese != "" || len(anime.TitleSynonyms) > 0 || len(anime.Studios) > 0 || len(anime.Producers) > 0 || anime.Source != "" || len(anime.Demographics) > 0 || len(anime.Themes) > 0 || anime.Broadcast.String != "" || len(anime.Streaming) > 0
|
|
}
|