Files
mal/web/components/anime_card.templ

102 lines
4.0 KiB
Plaintext

package ui
import "fmt"
type AnimeCardProps struct {
ID int
Title string
ImageURL string
Href 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
HideTitle bool // if true, do not render the default title block
CurrentNode bool // if true, renders a div instead of an anchor tag (useful for graph nodes)
Synopsis string // optional synopsis for hover detail
Score int // optional score for hover detail
PlayHref string // optional play button href (anchored to poster)
}
templ AnimeCard(props AnimeCardProps) {
if props.CurrentNode {
<div class={ defaultString(props.Class, "group relative min-w-0") }>
@animeCardPoster(props)
if !props.HideTitle {
<div class={ defaultString(props.TitleClass, "mt-2 line-clamp-2 text-sm leading-snug text-(--text)") }>
{ props.Title }
</div>
}
{ children... }
</div>
} else {
<div class={ defaultString(props.Class, "group relative min-w-0") }>
@animeCardPoster(props)
<a href={ templ.URL(cardHref(props)) } class="block bg-transparent text-inherit no-underline">
if !props.HideTitle {
<div class={ defaultString(props.TitleClass, "mt-2 line-clamp-2 text-sm leading-snug text-(--text)") }>
{ props.Title }
</div>
}
{ children... }
</a>
</div>
}
}
func cardHref(props AnimeCardProps) string {
if props.Href != "" {
return props.Href
}
return fmt.Sprintf("/anime/%d", props.ID)
}
templ animeCardPoster(props AnimeCardProps) {
<div class="relative w-full aspect-2/3 overflow-hidden">
<a href={ templ.URL(cardHref(props)) } class="block h-full w-full">
if props.ImageURL != "" {
<img src={ props.ImageURL } alt={ props.Title } class={ defaultString(props.ImgClass, "block h-full w-full object-cover object-center") } loading="lazy"/>
} else {
<div class="flex h-full w-full justify-center overflow-hidden text-transparent">No image</div>
}
</a>
<div class="absolute inset-0 bg-black/0 transition-colors duration-200 group-hover:bg-black/20"></div>
if props.Synopsis != "" || props.Score > 0 {
<div class="absolute inset-0 flex flex-col justify-between p-3 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
<div>
if props.Score > 0 {
<div class="mb-1.5 inline-flex items-center gap-1 rounded bg-(--accent)/90 px-1.5 py-0.5 text-[10px] font-medium text-(--text-on-accent)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="h-3 w-3">
<path fill-rule="evenodd" d="M7.43 1.466c-.924 1.53-.524 3.577.972 4.405l1.563.938a1.25 1.25 0 0 1 .457 1.666l-1.023 1.845a1.25 1.25 0 0 0 1.457 1.666l1.845-1.023a1.25 1.25 0 0 1 1.666.457l.938 1.563c.828 1.448 2.875 1.396 3.905-.1.714-1.036.714-2.384 0-3.42l-.938-1.563a1.25 1.25 0 0 1 .457-1.666l1.023-1.845a1.25 1.25 0 0 0-1.457-1.666l-1.845 1.023a1.25 1.25 0 0 1-1.666-.457l-.938-1.563c-.83-1.48.129-3.577 1.653-4.405.924-.5 1.696-1.135 2.268-1.89a6.25 6.25 0 0 0-4.834-3.415L8.722.216a.25.25 0 0 0-.427 0L5.84 1.384a6.25 6.25 0 0 0-4.834 3.415c.572.755 1.344 1.39 2.268 1.89Z" clip-rule="evenodd" />
</svg>
{ fmt.Sprintf("%d", props.Score) }
</div>
}
if props.Synopsis != "" {
<p class="line-clamp-3 text-[11px] leading-relaxed text-white/90">{ props.Synopsis }</p>
}
</div>
</div>
}
if props.PlayHref != "" {
<a
href={ templ.URL(props.PlayHref) }
class="absolute bottom-2 left-2 text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100"
aria-label="Play"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5V19L19 12L8 5Z" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>
</a>
}
</div>
}
func defaultString(val, fallback string) string {
if val == "" {
return fallback
}
return val
}