feat: add discover page for airing anime
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
51
internal/jikan/seasons.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
53
internal/templates/discovery.templ
Normal file
53
internal/templates/discovery.templ
Normal 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>
|
||||
}
|
||||
}
|
||||
}
|
||||
128
internal/templates/discovery_templ.go
Normal file
128
internal/templates/discovery_templ.go
Normal 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
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
>×</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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user