feat: add discover page for airing anime

This commit is contained in:
2026-04-07 14:08:08 +02:00
parent aa5a99eec7
commit f77788588e
19 changed files with 437 additions and 125 deletions

View File

@@ -172,3 +172,41 @@ func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(output)
}
func (h *Handler) HandleDiscover(w http.ResponseWriter, r *http.Request) {
templates.Discover().Render(r.Context(), w)
}
func (h *Handler) HandleAPIDiscoverAiring(w http.ResponseWriter, r *http.Request) {
pageStr := r.URL.Query().Get("page")
page, _ := strconv.Atoi(pageStr)
if page < 1 {
page = 1
}
res, err := h.svc.GetAiringAnime(page)
if err != nil {
log.Printf("airing anime error: %v", err)
http.Error(w, "Failed to fetch airing anime", http.StatusInternalServerError)
return
}
templates.DiscoverItems(res.Animes, "airing", page+1, res.HasNextPage).Render(r.Context(), w)
}
func (h *Handler) HandleAPIDiscoverUpcoming(w http.ResponseWriter, r *http.Request) {
pageStr := r.URL.Query().Get("page")
page, _ := strconv.Atoi(pageStr)
if page < 1 {
page = 1
}
res, err := h.svc.GetUpcomingAnime(page)
if err != nil {
log.Printf("upcoming anime error: %v", err)
http.Error(w, "Failed to fetch upcoming anime", http.StatusInternalServerError)
return
}
templates.DiscoverItems(res.Animes, "upcoming", page+1, res.HasNextPage).Render(r.Context(), w)
}

View File

@@ -28,6 +28,14 @@ func (s *Service) GetTopAnime(page int) (jikan.TopAnimeResult, error) {
return s.jikanClient.GetTopAnime(page)
}
func (s *Service) GetAiringAnime(page int) (jikan.TopAnimeResult, error) {
return s.jikanClient.GetSeasonsNow(page)
}
func (s *Service) GetUpcomingAnime(page int) (jikan.TopAnimeResult, error) {
return s.jikanClient.GetSeasonsUpcoming(page)
}
func (s *Service) GetAnimeDetails(ctx context.Context, id int, userID string) (jikan.Anime, string, error) {
anime, err := s.jikanClient.GetAnimeByID(id)
if err != nil {

View File

@@ -14,6 +14,8 @@ type Client struct {
baseURL string
cache *expirable.LRU[string, SearchResult]
topCache *expirable.LRU[int, TopAnimeResult]
airingCache *expirable.LRU[int, TopAnimeResult]
upcomingCache *expirable.LRU[int, TopAnimeResult]
animeCache *expirable.LRU[int, Anime]
relationsCache *expirable.LRU[int, JikanRelationsResponse]
}
@@ -21,6 +23,8 @@ type Client struct {
func NewClient() *Client {
cache := expirable.NewLRU[string, SearchResult](500, nil, time.Hour*1)
topCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1)
airingCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1)
upcomingCache := expirable.NewLRU[int, TopAnimeResult](100, nil, time.Hour*1)
animeCache := expirable.NewLRU[int, Anime](1000, nil, time.Hour*24)
relationsCache := expirable.NewLRU[int, JikanRelationsResponse](1000, nil, time.Hour*24)
@@ -29,6 +33,8 @@ func NewClient() *Client {
baseURL: "https://api.jikan.moe/v4",
cache: cache,
topCache: topCache,
airingCache: airingCache,
upcomingCache: upcomingCache,
animeCache: animeCache,
relationsCache: relationsCache,
}

51
internal/jikan/seasons.go Normal file
View File

@@ -0,0 +1,51 @@
package jikan
import "fmt"
// GetSeasonsNow fetches currently airing anime
func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) {
if page < 1 {
page = 1
}
if cached, ok := c.airingCache.Get(page); ok {
return cached, nil
}
var result TopAnimeResponse
reqURL := fmt.Sprintf("%s/seasons/now?page=%d", c.baseURL, page)
if err := c.fetchWithRetry(reqURL, &result); err != nil {
return TopAnimeResult{}, err
}
res := TopAnimeResult{
Animes: result.Data,
HasNextPage: result.Pagination.HasNextPage,
}
c.airingCache.Add(page, res)
return res, nil
}
// GetSeasonsUpcoming fetches upcoming anime
func (c *Client) GetSeasonsUpcoming(page int) (TopAnimeResult, error) {
if page < 1 {
page = 1
}
if cached, ok := c.upcomingCache.Get(page); ok {
return cached, nil
}
var result TopAnimeResponse
reqURL := fmt.Sprintf("%s/seasons/upcoming?page=%d", c.baseURL, page)
if err := c.fetchWithRetry(reqURL, &result); err != nil {
return TopAnimeResult{}, err
}
res := TopAnimeResult{
Animes: result.Data,
HasNextPage: result.Pagination.HasNextPage,
}
c.upcomingCache.Add(page, res)
return res, nil
}

View File

@@ -34,6 +34,9 @@ func NewRouter(cfg Config) http.Handler {
// Anime / Search / Catalog
mux.HandleFunc("/", animeHandler.HandleCatalog)
mux.HandleFunc("/discover", animeHandler.HandleDiscover)
mux.HandleFunc("/api/discover/airing", animeHandler.HandleAPIDiscoverAiring)
mux.HandleFunc("/api/discover/upcoming", animeHandler.HandleAPIDiscoverUpcoming)
mux.HandleFunc("/search", animeHandler.HandleSearch)
mux.HandleFunc("/api/search", animeHandler.HandleAPISearch)
mux.HandleFunc("/api/search-quick", animeHandler.HandleQuickSearch)

View File

@@ -11,7 +11,7 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) {
<div class="anime-hero">
<div class="anime-poster">
if anime.ImageURL() != "" {
<img src={ anime.ImageURL() } alt={ anime.DisplayTitle() } />
<img src={ anime.ImageURL() } alt={ anime.DisplayTitle() }/>
} else {
<div class="no-image">no image</div>
}
@@ -116,54 +116,54 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) {
<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>
}
if len(anime.Producers) > 0 {
<div class="sidebar-row">
<span class="sidebar-label">Producers</span>
<span class="sidebar-value">{ joinNames(anime.Producers) }</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>
}
}
if anime.Source != "" {
<div class="sidebar-row">
<span class="sidebar-label">Source</span>
<span class="sidebar-value">{ anime.Source }</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>
}
}
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>
</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>
}
</aside>
</div>
<script>
@@ -206,7 +206,7 @@ templ WatchlistDropdown(animeID int, animeTitle string, animeTitleEnglish string
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "plan_to_watch", currentStatus, airing)
if currentStatus != "" {
<div class="dropdown-divider"></div>
<button
<button
class="dropdown-item remove"
hx-delete={ string(templ.URL(fmt.Sprintf("/api/watchlist/%d", animeID))) }
hx-target="#watchlist-dropdown"
@@ -218,7 +218,7 @@ templ WatchlistDropdown(animeID int, animeTitle string, animeTitleEnglish string
}
templ dropdownStatusOption(animeID int, animeTitle string, animeTitleEnglish string, animeTitleJapanese string, animeImage string, status string, currentStatus string, airing bool) {
<button
<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) }
@@ -233,7 +233,7 @@ templ dropdownStatusOption(animeID int, animeTitle string, animeTitleEnglish str
}
templ statusOption(anime jikan.Anime, status string, currentStatus string) {
<button
<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) }
@@ -272,7 +272,7 @@ templ AnimeRelationsList(relations []jikan.RelationEntry) {
if rel.IsCurrent {
<div class="relation-card current">
if rel.Anime.ImageURL() != "" {
<img src={ rel.Anime.ImageURL() } alt={ rel.Anime.DisplayTitle() } class="relation-thumb" />
<img src={ rel.Anime.ImageURL() } alt={ rel.Anime.DisplayTitle() } class="relation-thumb"/>
} else {
<div class="no-image">no image</div>
}
@@ -281,7 +281,7 @@ templ AnimeRelationsList(relations []jikan.RelationEntry) {
} 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" />
<img src={ rel.Anime.ImageURL() } alt={ rel.Anime.DisplayTitle() } class="relation-thumb"/>
} else {
<div class="no-image">no image</div>
}
@@ -291,4 +291,4 @@ templ AnimeRelationsList(relations []jikan.RelationEntry) {
}
</div>
}
}
}

View File

@@ -444,7 +444,7 @@ func AnimeDetails(anime jikan.Anime, currentStatus string) templ.Component {
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(joinNames(anime.Producers))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 122, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 122, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
@@ -463,7 +463,7 @@ func AnimeDetails(anime jikan.Anime, currentStatus string) templ.Component {
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(anime.Source)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 128, Col: 47}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 128, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
@@ -487,7 +487,7 @@ func AnimeDetails(anime jikan.Anime, currentStatus string) templ.Component {
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(d.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 136, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 136, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
@@ -516,7 +516,7 @@ func AnimeDetails(anime jikan.Anime, currentStatus string) templ.Component {
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(t.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 146, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 146, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@@ -540,7 +540,7 @@ func AnimeDetails(anime jikan.Anime, currentStatus string) templ.Component {
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(anime.Broadcast.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 154, Col: 57}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 154, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
@@ -564,7 +564,7 @@ func AnimeDetails(anime jikan.Anime, currentStatus string) templ.Component {
var templ_7745c5c3_Var27 templ.SafeURL
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(s.URL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 162, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 162, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
@@ -577,7 +577,7 @@ func AnimeDetails(anime jikan.Anime, currentStatus string) templ.Component {
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(s.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 162, Col: 65}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 162, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {

View File

@@ -8,11 +8,11 @@ templ Login() {
<form action="/login" method="POST" class="login-form">
<div class="form-group">
<label for="username">email</label>
<input type="text" id="username" name="username" required placeholder="you@example.com" />
<input type="text" id="username" name="username" required placeholder="you@example.com"/>
</div>
<div class="form-group">
<label for="password">password</label>
<input type="password" id="password" name="password" required placeholder="your password" />
<input type="password" id="password" name="password" required placeholder="your password"/>
</div>
<button type="submit" class="login-button">sign in</button>
</form>

View File

@@ -35,7 +35,7 @@ templ CatalogItems(animes []jikan.Anime, nextPage int, hasNext bool) {
templ CatalogItem(anime jikan.Anime) {
<a href={ templ.URL(fmt.Sprintf("/anime/%d", anime.MalID)) }>
if anime.ImageURL() != "" {
<img src={ anime.ImageURL() } alt={ anime.DisplayTitle() } class="catalog-thumb" loading="lazy" />
<img src={ anime.ImageURL() } alt={ anime.DisplayTitle() } class="catalog-thumb" loading="lazy"/>
} else {
<div class="no-image">no image</div>
}
@@ -43,4 +43,4 @@ templ CatalogItem(anime jikan.Anime) {
<div class="catalog-title">
{ anime.DisplayTitle() }
</div>
}
}

View File

@@ -0,0 +1,53 @@
package templates
import "mal/internal/jikan"
import "fmt"
templ Discover() {
@Layout("mal - discover") {
<div class="discover-container">
<div class="tabs">
<button
class="tab active"
hx-get="/api/discover/airing?page=1"
hx-target="#discover-content"
hx-trigger="click"
onclick="document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); this.classList.add('active');"
>
airing now
</button>
<button
class="tab"
hx-get="/api/discover/upcoming?page=1"
hx-target="#discover-content"
hx-trigger="click"
onclick="document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); this.classList.add('active');"
>
upcoming
</button>
</div>
<div class="catalog-grid" id="discover-content" hx-get="/api/discover/airing?page=1" hx-trigger="load">
<div class="loading-indicator" style="grid-column: 1 / -1;">
<div class="loading-dot"></div>
<div class="loading-dot"></div>
<div class="loading-dot"></div>
<span>loading discover</span>
</div>
</div>
</div>
}
}
templ DiscoverItems(animes []jikan.Anime, listType string, nextPage int, hasNext bool) {
for i, anime := range animes {
if i == len(animes)-1 && hasNext {
<div class="catalog-item" hx-get={ string(templ.URL(fmt.Sprintf("/api/discover/%s?page=%d", listType, nextPage))) } hx-trigger="revealed" hx-swap="afterend">
@CatalogItem(anime)
</div>
} else {
<div class="catalog-item">
@CatalogItem(anime)
</div>
}
}
}

View File

@@ -0,0 +1,128 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "mal/internal/jikan"
import "fmt"
func Discover() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"discover-container\"><div class=\"tabs\"><button class=\"tab active\" hx-get=\"/api/discover/airing?page=1\" hx-target=\"#discover-content\" hx-trigger=\"click\" onclick=\"document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); this.classList.add('active');\">airing now</button> <button class=\"tab\" hx-get=\"/api/discover/upcoming?page=1\" hx-target=\"#discover-content\" hx-trigger=\"click\" onclick=\"document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); this.classList.add('active');\">upcoming</button></div><div class=\"catalog-grid\" id=\"discover-content\" hx-get=\"/api/discover/airing?page=1\" hx-trigger=\"load\"><div class=\"loading-indicator\" style=\"grid-column: 1 / -1;\"><div class=\"loading-dot\"></div><div class=\"loading-dot\"></div><div class=\"loading-dot\"></div><span>loading discover</span></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = Layout("mal - discover").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func DiscoverItems(animes []jikan.Anime, listType string, nextPage int, hasNext bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
for i, anime := range animes {
if i == len(animes)-1 && hasNext {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"catalog-item\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/api/discover/%s?page=%d", listType, nextPage))))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/discovery.templ`, Line: 44, Col: 116}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" hx-trigger=\"revealed\" hx-swap=\"afterend\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = CatalogItem(anime).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"catalog-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = CatalogItem(anime).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,8 +1,8 @@
package templates
import (
"mal/internal/jikan"
"fmt"
"mal/internal/jikan"
"net/url"
)
@@ -50,4 +50,4 @@ templ SearchItems(query string, animes []jikan.Anime, nextPage int, hasNext bool
</div>
}
}
}
}

View File

@@ -17,23 +17,22 @@ templ Layout(title string) {
<a href="/" class="logo">/mal</a>
<div class="nav">
<a href="/">catalog</a>
<a href="/discover">discover</a>
<a href="/watchlist">watchlist</a>
</div>
</div>
<div class="header-search-wrapper">
<form action="/search" method="GET" class="header-search">
<input type="text" id="search-input" name="q" class="search-input" placeholder="search anime..." autocomplete="off" />
<input type="text" id="search-input" name="q" class="search-input" placeholder="search anime..." autocomplete="off"/>
<div id="search-dropdown" class="search-dropdown"></div>
</form>
</div>
</div>
</header>
<main>
{ children... }
</main>
<script src="/static/js/search.js"></script>
</body>
</html>
}
}

View File

@@ -42,7 +42,7 @@ func Layout(title string) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/static/css/style.css\"><script src=\"https://unpkg.com/htmx.org@1.9.11\"></script></head><body><header><div class=\"header-top\"><div class=\"header-left\"><a href=\"/\" class=\"logo\">/mal</a><div class=\"nav\"><a href=\"/\">catalog</a> <a href=\"/watchlist\">watchlist</a></div></div><div class=\"header-search-wrapper\"><form action=\"/search\" method=\"GET\" class=\"header-search\"><input type=\"text\" id=\"search-input\" name=\"q\" class=\"search-input\" placeholder=\"search anime...\" autocomplete=\"off\"><div id=\"search-dropdown\" class=\"search-dropdown\"></div></form></div></div></header><main>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/static/css/style.css\"><script src=\"https://unpkg.com/htmx.org@1.9.11\"></script></head><body><header><div class=\"header-top\"><div class=\"header-left\"><a href=\"/\" class=\"logo\">/mal</a><div class=\"nav\"><a href=\"/\">catalog</a> <a href=\"/discover\">discover</a> <a href=\"/watchlist\">watchlist</a></div></div><div class=\"header-search-wrapper\"><form action=\"/search\" method=\"GET\" class=\"header-search\"><input type=\"text\" id=\"search-input\" name=\"q\" class=\"search-input\" placeholder=\"search anime...\" autocomplete=\"off\"><div id=\"search-dropdown\" class=\"search-dropdown\"></div></form></div></div></header><main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@@ -1,9 +1,9 @@
package templates
type SortFilterOptions struct {
Sort string // "title", "date", "score"
Order string // "asc", "desc"
View string // for watchlist: "grid", "table"
Sort string // "title", "date", "score"
Order string // "asc", "desc"
View string // for watchlist: "grid", "table"
Status string // for watchlist: "all", "watching", etc
}
@@ -12,27 +12,27 @@ templ SortFilter(opts SortFilterOptions) {
<div class="sort-filter-group">
<label for="sort-select">sort by</label>
<select id="sort-select" class="sort-filter-select" onchange="document.getElementById('sort-input').value = this.value; document.getElementById('sort-form').submit()">
<option value="date" selected?={opts.Sort == "date"}>date added</option>
<option value="title" selected?={opts.Sort == "title"}>title</option>
<option value="score" selected?={opts.Sort == "score"}>score</option>
<option value="date" selected?={ opts.Sort == "date" }>date added</option>
<option value="title" selected?={ opts.Sort == "title" }>title</option>
<option value="score" selected?={ opts.Sort == "score" }>score</option>
</select>
</div>
<div class="sort-filter-group">
<label for="order-select">order</label>
<select id="order-select" class="sort-filter-select" onchange="document.getElementById('order-input').value = this.value; document.getElementById('sort-form').submit()">
<option value="desc" selected?={opts.Order == "desc"}>descending</option>
<option value="asc" selected?={opts.Order == "asc"}>ascending</option>
<option value="desc" selected?={ opts.Order == "desc" }>descending</option>
<option value="asc" selected?={ opts.Order == "asc" }>ascending</option>
</select>
</div>
</div>
<form id="sort-form" method="get" style="display: none;">
<input type="hidden" name="sort" id="sort-input" value={opts.Sort} />
<input type="hidden" name="order" id="order-input" value={opts.Order} />
<input type="hidden" name="sort" id="sort-input" value={ opts.Sort }/>
<input type="hidden" name="order" id="order-input" value={ opts.Order }/>
if opts.View != "" {
<input type="hidden" name="view" value={opts.View} />
<input type="hidden" name="view" value={ opts.View }/>
}
if opts.Status != "" {
<input type="hidden" name="status" value={opts.Status} />
<input type="hidden" name="status" value={ opts.Status }/>
}
</form>
}

View File

@@ -93,7 +93,7 @@ func SortFilter(opts SortFilterOptions) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(opts.Sort)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/sort_filter.templ`, Line: 29, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/sort_filter.templ`, Line: 29, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -106,7 +106,7 @@ func SortFilter(opts SortFilterOptions) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(opts.Order)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/sort_filter.templ`, Line: 30, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/sort_filter.templ`, Line: 30, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -124,7 +124,7 @@ func SortFilter(opts SortFilterOptions) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(opts.View)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/sort_filter.templ`, Line: 32, Col: 52}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/sort_filter.templ`, Line: 32, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -143,7 +143,7 @@ func SortFilter(opts SortFilterOptions) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(opts.Status)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/sort_filter.templ`, Line: 35, Col: 56}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/sort_filter.templ`, Line: 35, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {

View File

@@ -13,7 +13,7 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
<a href="/api/watchlist/export" class="text-link">export</a>
<button class="text-link" onclick="document.getElementById('import-file').click()">import</button>
<form id="import-form" hx-post="/api/watchlist/import" hx-encoding="multipart/form-data" style="display: none;">
<input type="file" id="import-file" name="file" accept=".json" onchange="htmx.trigger('#import-form', 'submit')" />
<input type="file" id="import-file" name="file" accept=".json" onchange="htmx.trigger('#import-form', 'submit')"/>
</form>
<div class="view-toggle">
<a href={ templ.URL(fmt.Sprintf("/watchlist?view=grid&status=%s&sort=%s&order=%s", currentStatus, sortBy, sortOrder)) } class={ viewClass(layout == "grid") }>grid</a>
@@ -21,7 +21,6 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
</div>
</div>
</div>
<div class="status-tabs">
<a href={ templ.URL(fmt.Sprintf("/watchlist?view=%s&status=all&sort=%s&order=%s", layout, sortBy, sortOrder)) } class={ tabClass(currentStatus == "all") }>all</a>
<a href={ templ.URL(fmt.Sprintf("/watchlist?view=%s&status=watching&sort=%s&order=%s", layout, sortBy, sortOrder)) } class={ tabClass(currentStatus == "watching") }>watching</a>
@@ -31,9 +30,7 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
<a href={ templ.URL(fmt.Sprintf("/watchlist?view=%s&status=dropped&sort=%s&order=%s", layout, sortBy, sortOrder)) } class={ tabClass(currentStatus == "dropped") }>dropped</a>
<a href={ templ.URL(fmt.Sprintf("/watchlist?view=%s&status=completed&sort=%s&order=%s", layout, sortBy, sortOrder)) } class={ tabClass(currentStatus == "completed") }>completed</a>
</div>
@SortFilter(SortFilterOptions{Sort: sortBy, Order: sortOrder, View: layout, Status: currentStatus})
if len(entries) == 0 {
<div class="empty-state">
<div class="empty-state-title">nothing here yet</div>
@@ -54,16 +51,16 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
<div class="catalog-item watchlist-item" id={ fmt.Sprintf("watchlist-entry-%d", entry.AnimeID) }>
<a href={ templ.URL(fmt.Sprintf("/anime/%d", entry.AnimeID)) }>
if entry.ImageUrl != "" {
<img src={ entry.ImageUrl } alt={ entry.DisplayTitle() } class="catalog-thumb" loading="lazy" />
<img src={ entry.ImageUrl } alt={ entry.DisplayTitle() } class="catalog-thumb" loading="lazy"/>
} else {
<div class="no-image">no image</div>
}
</a>
<div class="catalog-title">{ entry.DisplayTitle() }</div>
<button
<button
class="remove-btn"
hx-delete={ string(templ.URL(fmt.Sprintf("/api/watchlist/%d?from=watchlist", entry.AnimeID))) }
hx-target={ fmt.Sprintf("#watchlist-entry-%d", entry.AnimeID) }
hx-delete={ string(templ.URL(fmt.Sprintf("/api/watchlist/%d?from=watchlist", entry.AnimeID))) }
hx-target={ fmt.Sprintf("#watchlist-entry-%d", entry.AnimeID) }
hx-swap="delete"
>&times;</button>
</div>
@@ -86,16 +83,16 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
<img src={ entry.ImageUrl } alt={ entry.DisplayTitle() } class="thumb" loading="lazy"/>
</a>
</td>
<td class="title-cell">
<a href={ templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID)) }>
{ entry.DisplayTitle() }
</a>
</td>
<td class="actions-cell">
<button
<td class="title-cell">
<a href={ templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID)) }>
{ entry.DisplayTitle() }
</a>
</td>
<td class="actions-cell">
<button
class="remove-link"
hx-delete={ string(templ.URL(fmt.Sprintf("/api/watchlist/%d?from=watchlist", entry.AnimeID))) }
hx-target={ fmt.Sprintf("#watchlist-entry-%d", entry.AnimeID) }
hx-delete={ string(templ.URL(fmt.Sprintf("/api/watchlist/%d?from=watchlist", entry.AnimeID))) }
hx-target={ fmt.Sprintf("#watchlist-entry-%d", entry.AnimeID) }
hx-swap="delete"
style="background: none; border: none; cursor: pointer;"
>remove</button>

View File

@@ -132,7 +132,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var10 templ.SafeURL
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=all&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 26, Col: 112}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 25, Col: 112}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -167,7 +167,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=watching&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 27, Col: 117}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 26, Col: 117}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -202,7 +202,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var16 templ.SafeURL
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=continuing&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 28, Col: 119}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 27, Col: 119}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@@ -237,7 +237,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var19 templ.SafeURL
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=on_hold&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 29, Col: 116}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 28, Col: 116}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
@@ -272,7 +272,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var22 templ.SafeURL
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=plan_to_watch&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 30, Col: 122}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 29, Col: 122}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
@@ -307,7 +307,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var25 templ.SafeURL
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=dropped&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 31, Col: 116}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 30, Col: 116}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@@ -342,7 +342,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var28 templ.SafeURL
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/watchlist?view=%s&status=completed&sort=%s&order=%s", layout, sortBy, sortOrder)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 32, Col: 118}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 31, Col: 118}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
@@ -412,7 +412,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("watchlist-entry-%d", entry.AnimeID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 54, Col: 100}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 51, Col: 100}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
@@ -425,7 +425,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var31 templ.SafeURL
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/anime/%d", entry.AnimeID)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 55, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 52, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
@@ -443,7 +443,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(entry.ImageUrl)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 57, Col: 34}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 54, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
@@ -456,7 +456,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(entry.DisplayTitle())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 57, Col: 63}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 54, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
if templ_7745c5c3_Err != nil {
@@ -479,7 +479,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(entry.DisplayTitle())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 62, Col: 56}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 59, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil {
@@ -492,7 +492,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/api/watchlist/%d?from=watchlist", entry.AnimeID))))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 65, Col: 101}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 62, Col: 101}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
if templ_7745c5c3_Err != nil {
@@ -505,7 +505,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("#watchlist-entry-%d", entry.AnimeID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 66, Col: 69}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 63, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil {
@@ -533,7 +533,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("watchlist-entry-%d", entry.AnimeID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 83, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 80, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil {
@@ -546,7 +546,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var38 templ.SafeURL
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 85, Col: 73}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 82, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
if templ_7745c5c3_Err != nil {
@@ -559,7 +559,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(entry.ImageUrl)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 86, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 83, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
if templ_7745c5c3_Err != nil {
@@ -572,7 +572,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var40 string
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(entry.DisplayTitle())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 86, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 83, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
if templ_7745c5c3_Err != nil {
@@ -585,7 +585,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var41 templ.SafeURL
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 90, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 87, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil {
@@ -598,7 +598,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var42 string
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(entry.DisplayTitle())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 91, Col: 31}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 88, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
if templ_7745c5c3_Err != nil {
@@ -611,7 +611,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/api/watchlist/%d?from=watchlist", entry.AnimeID))))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 97, Col: 103}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 94, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
if templ_7745c5c3_Err != nil {
@@ -624,7 +624,7 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
var templ_7745c5c3_Var44 string
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("#watchlist-entry-%d", entry.AnimeID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 98, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 95, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
if templ_7745c5c3_Err != nil {

View File

@@ -973,3 +973,32 @@ a:visited {
gap: 8px;
}
}
/* Tabs */
.tabs {
display: flex;
gap: 16px;
margin-bottom: 24px;
border-bottom: 1px solid var(--border);
padding-bottom: 8px;
}
.tab {
background: none;
border: none;
color: var(--text-muted);
font-size: 16px;
cursor: pointer;
padding: 8px 16px;
transition: color 0.2s;
text-transform: lowercase;
}
.tab:hover {
color: var(--text);
}
.tab.active {
color: var(--link);
font-weight: bold;
}