diff --git a/internal/database/models.go b/internal/database/models.go
index ef4861d..80abb98 100644
--- a/internal/database/models.go
+++ b/internal/database/models.go
@@ -27,6 +27,13 @@ type Anime struct {
Airing sql.NullBool `json:"airing"`
}
+type NotificationPreference struct {
+ ID string `json:"id"`
+ UserID string `json:"user_id"`
+ NotifyNewEpisodes bool `json:"notify_new_episodes"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
type Session struct {
ID string `json:"id"`
UserID string `json:"user_id"`
@@ -42,10 +49,12 @@ type User struct {
}
type WatchListEntry struct {
- ID string `json:"id"`
- UserID string `json:"user_id"`
- AnimeID int64 `json:"anime_id"`
- Status string `json:"status"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
+ ID string `json:"id"`
+ UserID string `json:"user_id"`
+ AnimeID int64 `json:"anime_id"`
+ Status string `json:"status"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ CurrentEpisode sql.NullInt64 `json:"current_episode"`
+ LastEpisodeAt sql.NullTime `json:"last_episode_at"`
}
diff --git a/internal/database/querier.go b/internal/database/querier.go
index e5dd7c6..eed23f7 100644
--- a/internal/database/querier.go
+++ b/internal/database/querier.go
@@ -20,6 +20,7 @@ type Querier interface {
GetUserByUsername(ctx context.Context, username string) (User, error)
GetUserWatchList(ctx context.Context, userID string) ([]GetUserWatchListRow, error)
GetWatchListEntry(ctx context.Context, arg GetWatchListEntryParams) (WatchListEntry, error)
+ GetWatchingAnime(ctx context.Context, userID string) ([]GetWatchingAnimeRow, error)
UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime, error)
UpsertWatchListEntry(ctx context.Context, arg UpsertWatchListEntryParams) (WatchListEntry, error)
}
diff --git a/internal/database/queries.sql b/internal/database/queries.sql
index e35f9f9..4aff98c 100644
--- a/internal/database/queries.sql
+++ b/internal/database/queries.sql
@@ -38,10 +38,11 @@ RETURNING *;
SELECT * FROM anime WHERE id = ? LIMIT 1;
-- name: UpsertWatchListEntry :one
-INSERT INTO watch_list_entry (id, user_id, anime_id, status, updated_at)
-VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
+INSERT INTO watch_list_entry (id, user_id, anime_id, status, current_episode, updated_at)
+VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT (user_id, anime_id) DO UPDATE SET
status = excluded.status,
+ current_episode = excluded.current_episode,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
@@ -65,3 +66,16 @@ ORDER BY e.updated_at DESC;
-- name: DeleteWatchListEntry :exec
DELETE FROM watch_list_entry
WHERE user_id = ? AND anime_id = ?;
+
+-- name: GetWatchingAnime :many
+SELECT
+ e.*,
+ a.title_original,
+ a.title_english,
+ a.title_japanese,
+ a.image_url,
+ a.airing
+FROM watch_list_entry e
+JOIN anime a ON e.anime_id = a.id
+WHERE e.user_id = ? AND e.status = 'watching' AND a.airing = 1
+ORDER BY e.updated_at DESC;
diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go
index 735d106..81b533b 100644
--- a/internal/database/queries.sql.go
+++ b/internal/database/queries.sql.go
@@ -161,7 +161,7 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
const getUserWatchList = `-- name: GetUserWatchList :many
SELECT
- e.id, e.user_id, e.anime_id, e.status, e.created_at, e.updated_at,
+ e.id, e.user_id, e.anime_id, e.status, e.created_at, e.updated_at, e.current_episode, e.last_episode_at,
a.title_original,
a.title_english,
a.title_japanese,
@@ -174,17 +174,19 @@ ORDER BY e.updated_at DESC
`
type GetUserWatchListRow struct {
- ID string `json:"id"`
- UserID string `json:"user_id"`
- AnimeID int64 `json:"anime_id"`
- Status string `json:"status"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
- TitleOriginal string `json:"title_original"`
- TitleEnglish sql.NullString `json:"title_english"`
- TitleJapanese sql.NullString `json:"title_japanese"`
- ImageUrl string `json:"image_url"`
- Airing sql.NullBool `json:"airing"`
+ ID string `json:"id"`
+ UserID string `json:"user_id"`
+ AnimeID int64 `json:"anime_id"`
+ Status string `json:"status"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ CurrentEpisode sql.NullInt64 `json:"current_episode"`
+ LastEpisodeAt sql.NullTime `json:"last_episode_at"`
+ TitleOriginal string `json:"title_original"`
+ TitleEnglish sql.NullString `json:"title_english"`
+ TitleJapanese sql.NullString `json:"title_japanese"`
+ ImageUrl string `json:"image_url"`
+ Airing sql.NullBool `json:"airing"`
}
func (q *Queries) GetUserWatchList(ctx context.Context, userID string) ([]GetUserWatchListRow, error) {
@@ -203,6 +205,8 @@ func (q *Queries) GetUserWatchList(ctx context.Context, userID string) ([]GetUse
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.CurrentEpisode,
+ &i.LastEpisodeAt,
&i.TitleOriginal,
&i.TitleEnglish,
&i.TitleJapanese,
@@ -223,7 +227,7 @@ func (q *Queries) GetUserWatchList(ctx context.Context, userID string) ([]GetUse
}
const getWatchListEntry = `-- name: GetWatchListEntry :one
-SELECT id, user_id, anime_id, status, created_at, updated_at FROM watch_list_entry
+SELECT id, user_id, anime_id, status, created_at, updated_at, current_episode, last_episode_at FROM watch_list_entry
WHERE user_id = ? AND anime_id = ? LIMIT 1
`
@@ -242,10 +246,79 @@ func (q *Queries) GetWatchListEntry(ctx context.Context, arg GetWatchListEntryPa
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.CurrentEpisode,
+ &i.LastEpisodeAt,
)
return i, err
}
+const getWatchingAnime = `-- name: GetWatchingAnime :many
+SELECT
+ e.id, e.user_id, e.anime_id, e.status, e.created_at, e.updated_at, e.current_episode, e.last_episode_at,
+ a.title_original,
+ a.title_english,
+ a.title_japanese,
+ a.image_url,
+ a.airing
+FROM watch_list_entry e
+JOIN anime a ON e.anime_id = a.id
+WHERE e.user_id = ? AND e.status = 'watching' AND a.airing = 1
+ORDER BY e.updated_at DESC
+`
+
+type GetWatchingAnimeRow struct {
+ ID string `json:"id"`
+ UserID string `json:"user_id"`
+ AnimeID int64 `json:"anime_id"`
+ Status string `json:"status"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ CurrentEpisode sql.NullInt64 `json:"current_episode"`
+ LastEpisodeAt sql.NullTime `json:"last_episode_at"`
+ TitleOriginal string `json:"title_original"`
+ TitleEnglish sql.NullString `json:"title_english"`
+ TitleJapanese sql.NullString `json:"title_japanese"`
+ ImageUrl string `json:"image_url"`
+ Airing sql.NullBool `json:"airing"`
+}
+
+func (q *Queries) GetWatchingAnime(ctx context.Context, userID string) ([]GetWatchingAnimeRow, error) {
+ rows, err := q.db.QueryContext(ctx, getWatchingAnime, userID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []GetWatchingAnimeRow
+ for rows.Next() {
+ var i GetWatchingAnimeRow
+ if err := rows.Scan(
+ &i.ID,
+ &i.UserID,
+ &i.AnimeID,
+ &i.Status,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ &i.CurrentEpisode,
+ &i.LastEpisodeAt,
+ &i.TitleOriginal,
+ &i.TitleEnglish,
+ &i.TitleJapanese,
+ &i.ImageUrl,
+ &i.Airing,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const upsertAnime = `-- name: UpsertAnime :one
INSERT INTO anime (id, title_original, title_english, title_japanese, image_url, airing)
VALUES (?, ?, ?, ?, ?, ?)
@@ -290,19 +363,21 @@ func (q *Queries) UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime
}
const upsertWatchListEntry = `-- name: UpsertWatchListEntry :one
-INSERT INTO watch_list_entry (id, user_id, anime_id, status, updated_at)
-VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
+INSERT INTO watch_list_entry (id, user_id, anime_id, status, current_episode, updated_at)
+VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT (user_id, anime_id) DO UPDATE SET
status = excluded.status,
+ current_episode = excluded.current_episode,
updated_at = CURRENT_TIMESTAMP
-RETURNING id, user_id, anime_id, status, created_at, updated_at
+RETURNING id, user_id, anime_id, status, created_at, updated_at, current_episode, last_episode_at
`
type UpsertWatchListEntryParams struct {
- ID string `json:"id"`
- UserID string `json:"user_id"`
- AnimeID int64 `json:"anime_id"`
- Status string `json:"status"`
+ ID string `json:"id"`
+ UserID string `json:"user_id"`
+ AnimeID int64 `json:"anime_id"`
+ Status string `json:"status"`
+ CurrentEpisode sql.NullInt64 `json:"current_episode"`
}
func (q *Queries) UpsertWatchListEntry(ctx context.Context, arg UpsertWatchListEntryParams) (WatchListEntry, error) {
@@ -311,6 +386,7 @@ func (q *Queries) UpsertWatchListEntry(ctx context.Context, arg UpsertWatchListE
arg.UserID,
arg.AnimeID,
arg.Status,
+ arg.CurrentEpisode,
)
var i WatchListEntry
err := row.Scan(
@@ -320,6 +396,8 @@ func (q *Queries) UpsertWatchListEntry(ctx context.Context, arg UpsertWatchListE
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.CurrentEpisode,
+ &i.LastEpisodeAt,
)
return i, err
}
diff --git a/internal/features/anime/handler.go b/internal/features/anime/handler.go
index 55035d5..5a0fe40 100644
--- a/internal/features/anime/handler.go
+++ b/internal/features/anime/handler.go
@@ -145,6 +145,62 @@ func (h *Handler) HandleAPIAnimeRelations(w http.ResponseWriter, r *http.Request
templates.AnimeRelationsList(relations).Render(r.Context(), w)
}
+// HandleAPIAnime routes anime API requests
+func (h *Handler) HandleAPIAnime(w http.ResponseWriter, r *http.Request) {
+ path := r.URL.Path[len("/api/anime/"):]
+
+ // Parse: {id}/relations or {id}/recommendations
+ parts := splitPath(path)
+ if len(parts) < 2 {
+ http.Error(w, "invalid path", http.StatusBadRequest)
+ return
+ }
+
+ id, err := strconv.Atoi(parts[0])
+ if err != nil || id <= 0 {
+ http.Error(w, "invalid id", http.StatusBadRequest)
+ return
+ }
+
+ switch parts[1] {
+ case "relations":
+ relations := h.svc.GetRelations(id)
+ templates.AnimeRelationsList(relations).Render(r.Context(), w)
+ case "recommendations":
+ recs, err := h.svc.GetRecommendations(id)
+ if err != nil {
+ log.Printf("recommendations error for %d: %v", id, err)
+ http.Error(w, "Failed to fetch recommendations", http.StatusInternalServerError)
+ return
+ }
+ if len(recs) > 10 {
+ recs = recs[:10]
+ }
+ templates.AnimeRecommendations(recs).Render(r.Context(), w)
+ default:
+ http.Error(w, "not found", http.StatusNotFound)
+ }
+}
+
+func splitPath(path string) []string {
+ var parts []string
+ var current string
+ for _, c := range path {
+ if c == '/' {
+ if current != "" {
+ parts = append(parts, current)
+ current = ""
+ }
+ } else {
+ current += string(c)
+ }
+ }
+ if current != "" {
+ parts = append(parts, current)
+ }
+ return parts
+}
+
func (h *Handler) HandleQuickSearch(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
@@ -231,3 +287,46 @@ func (h *Handler) HandleAPIDiscoverUpcoming(w http.ResponseWriter, r *http.Reque
templates.DiscoverItems(res.Animes, "upcoming", page+1, res.HasNextPage).Render(r.Context(), w)
}
+
+func (h *Handler) HandleSchedule(w http.ResponseWriter, r *http.Request) {
+ templates.Schedule().Render(r.Context(), w)
+}
+
+func (h *Handler) HandleAPISchedule(w http.ResponseWriter, r *http.Request) {
+ day := r.URL.Query().Get("day")
+ if day == "" {
+ day = "monday"
+ }
+
+ res, err := h.svc.GetSchedule(day)
+ if err != nil {
+ log.Printf("schedule error for %s: %v", day, err)
+ http.Error(w, "Failed to fetch schedule", http.StatusInternalServerError)
+ return
+ }
+
+ res.Animes = deduplicateAnimes(res.Animes)
+
+ templates.ScheduleDay(day, res.Animes).Render(r.Context(), w)
+}
+
+func (h *Handler) HandleNotifications(w http.ResponseWriter, r *http.Request) {
+ userID := ""
+ if user, ok := r.Context().Value(middleware.UserContextKey).(*database.User); ok && user != nil {
+ userID = user.ID
+ }
+
+ if userID == "" {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+
+ watching, err := h.svc.GetWatchingAnime(r.Context(), userID)
+ if err != nil {
+ log.Printf("watching anime error: %v", err)
+ http.Error(w, "Failed to fetch watching anime", http.StatusInternalServerError)
+ return
+ }
+
+ templates.Notifications(watching).Render(r.Context(), w)
+}
diff --git a/internal/features/anime/service.go b/internal/features/anime/service.go
index 7e60522..3f6313e 100644
--- a/internal/features/anime/service.go
+++ b/internal/features/anime/service.go
@@ -6,6 +6,7 @@ import (
"mal/internal/database"
"mal/internal/jikan"
+ "mal/internal/templates"
)
type Service struct {
@@ -59,3 +60,34 @@ func (s *Service) GetAnimeDetails(ctx context.Context, id int, userID string) (j
func (s *Service) GetRelations(id int) []jikan.RelationEntry {
return s.jikanClient.GetFullRelations(id)
}
+
+func (s *Service) GetSchedule(day string) (jikan.ScheduleResult, error) {
+ return s.jikanClient.GetSchedule(day)
+}
+
+func (s *Service) GetRecommendations(animeID int) ([]jikan.Anime, error) {
+ return s.jikanClient.GetRecommendations(animeID)
+}
+
+func (s *Service) GetWatchingAnime(ctx context.Context, userID string) ([]templates.WatchingAnimeWithDetails, error) {
+ rows, err := s.db.GetWatchingAnime(ctx, userID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get watching anime: %w", err)
+ }
+
+ var result []templates.WatchingAnimeWithDetails
+ for _, row := range rows {
+ anime, err := s.jikanClient.GetAnimeByID(int(row.AnimeID))
+ if err != nil {
+ // Skip if we can't fetch anime details
+ continue
+ }
+
+ result = append(result, templates.WatchingAnimeWithDetails{
+ Entry: row,
+ Anime: anime,
+ })
+ }
+
+ return result, nil
+}
diff --git a/internal/features/watchlist/service.go b/internal/features/watchlist/service.go
index b15f38b..0fc3518 100644
--- a/internal/features/watchlist/service.go
+++ b/internal/features/watchlist/service.go
@@ -48,10 +48,11 @@ func (s *Service) AddEntry(ctx context.Context, userID string, req AddRequest) e
entryID := uuid.New().String()
_, err = s.db.UpsertWatchListEntry(ctx, database.UpsertWatchListEntryParams{
- ID: entryID,
- UserID: userID,
- AnimeID: req.AnimeID,
- Status: req.Status,
+ ID: entryID,
+ UserID: userID,
+ AnimeID: req.AnimeID,
+ Status: req.Status,
+ CurrentEpisode: sql.NullInt64{Int64: 0, Valid: false},
})
if err != nil {
return fmt.Errorf("failed to update watchlist: %w", err)
@@ -152,10 +153,11 @@ func (s *Service) Import(ctx context.Context, userID string, export ExportData)
}
_, err = s.db.UpsertWatchListEntry(ctx, database.UpsertWatchListEntryParams{
- ID: uuid.New().String(),
- UserID: userID,
- AnimeID: entry.AnimeID,
- Status: entry.Status,
+ ID: uuid.New().String(),
+ UserID: userID,
+ AnimeID: entry.AnimeID,
+ Status: entry.Status,
+ CurrentEpisode: sql.NullInt64{Int64: 0, Valid: false},
})
if err != nil {
continue
diff --git a/internal/jikan/client.go b/internal/jikan/client.go
index 24cbdd0..b5f0f38 100644
--- a/internal/jikan/client.go
+++ b/internal/jikan/client.go
@@ -18,6 +18,8 @@ type Client struct {
upcomingCache *expirable.LRU[int, TopAnimeResult]
animeCache *expirable.LRU[int, Anime]
relationsCache *expirable.LRU[int, JikanRelationsResponse]
+ scheduleCache *expirable.LRU[string, ScheduleResult]
+ recsCache *expirable.LRU[int, []Anime]
}
func NewClient() *Client {
@@ -27,6 +29,8 @@ func NewClient() *Client {
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)
+ scheduleCache := expirable.NewLRU[string, ScheduleResult](50, nil, time.Hour*1)
+ recsCache := expirable.NewLRU[int, []Anime](500, nil, time.Hour*24)
return &Client{
httpClient: &http.Client{Timeout: 10 * time.Second},
@@ -37,6 +41,8 @@ func NewClient() *Client {
upcomingCache: upcomingCache,
animeCache: animeCache,
relationsCache: relationsCache,
+ scheduleCache: scheduleCache,
+ recsCache: recsCache,
}
}
diff --git a/internal/jikan/recommendations.go b/internal/jikan/recommendations.go
new file mode 100644
index 0000000..b38bb50
--- /dev/null
+++ b/internal/jikan/recommendations.go
@@ -0,0 +1,61 @@
+package jikan
+
+import "fmt"
+
+// RecommendationEntry represents a single recommendation
+type RecommendationEntry struct {
+ Entry struct {
+ MalID int `json:"mal_id"`
+ URL string `json:"url"`
+ Images struct {
+ Webp struct {
+ LargeImageURL string `json:"large_image_url"`
+ } `json:"webp"`
+ } `json:"images"`
+ Title string `json:"title"`
+ } `json:"entry"`
+ Votes int `json:"votes"`
+}
+
+type RecommendationsResponse struct {
+ Data []RecommendationEntry `json:"data"`
+}
+
+// GetRecommendations fetches recommendations for an anime
+func (c *Client) GetRecommendations(animeID int) ([]Anime, error) {
+ if cached, ok := c.recsCache.Get(animeID); ok {
+ return cached, nil
+ }
+
+ var result RecommendationsResponse
+ reqURL := fmt.Sprintf("%s/anime/%d/recommendations", c.baseURL, animeID)
+ if err := c.fetchWithRetry(reqURL, &result); err != nil {
+ return nil, err
+ }
+
+ // Convert to Anime slice (partial data)
+ animes := make([]Anime, 0, len(result.Data))
+ for _, rec := range result.Data {
+ animes = append(animes, Anime{
+ MalID: rec.Entry.MalID,
+ Title: rec.Entry.Title,
+ Images: struct {
+ Jpg struct {
+ LargeImageURL string `json:"large_image_url"`
+ } `json:"jpg"`
+ Webp struct {
+ LargeImageURL string `json:"large_image_url"`
+ } `json:"webp"`
+ }{
+ Webp: struct {
+ LargeImageURL string `json:"large_image_url"`
+ }{
+ LargeImageURL: rec.Entry.Images.Webp.LargeImageURL,
+ },
+ },
+ })
+ }
+
+ c.recsCache.Add(animeID, animes)
+ return animes, nil
+}
diff --git a/internal/jikan/seasons.go b/internal/jikan/seasons.go
index ffed9c6..acfb212 100644
--- a/internal/jikan/seasons.go
+++ b/internal/jikan/seasons.go
@@ -1,6 +1,56 @@
package jikan
-import "fmt"
+import (
+ "fmt"
+ "strings"
+)
+
+// ScheduleResult contains anime grouped by day
+type ScheduleResult struct {
+ Animes []Anime
+ HasNextPage bool
+}
+
+// GetSchedule fetches anime airing on a specific day
+// day can be: monday, tuesday, wednesday, thursday, friday, saturday, sunday, unknown, other
+func (c *Client) GetSchedule(day string) (ScheduleResult, error) {
+ day = strings.ToLower(day)
+ cacheKey := fmt.Sprintf("schedule_%s", day)
+
+ if cached, ok := c.scheduleCache.Get(cacheKey); ok {
+ return cached, nil
+ }
+
+ var result TopAnimeResponse
+ reqURL := fmt.Sprintf("%s/schedules?filter=%s&sfw=true", c.baseURL, day)
+ if err := c.fetchWithRetry(reqURL, &result); err != nil {
+ return ScheduleResult{}, err
+ }
+
+ res := ScheduleResult{
+ Animes: result.Data,
+ HasNextPage: result.Pagination.HasNextPage,
+ }
+
+ c.scheduleCache.Add(cacheKey, res)
+ return res, nil
+}
+
+// GetFullSchedule fetches all days at once
+func (c *Client) GetFullSchedule() (map[string][]Anime, error) {
+ days := []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}
+ schedule := make(map[string][]Anime)
+
+ for _, day := range days {
+ res, err := c.GetSchedule(day)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch %s schedule: %w", day, err)
+ }
+ schedule[day] = res.Animes
+ }
+
+ return schedule, nil
+}
// GetSeasonsNow fetches currently airing anime
func (c *Client) GetSeasonsNow(page int) (TopAnimeResult, error) {
diff --git a/internal/server/routes.go b/internal/server/routes.go
index 6960da0..2265e4b 100644
--- a/internal/server/routes.go
+++ b/internal/server/routes.go
@@ -35,6 +35,9 @@ func NewRouter(cfg Config) http.Handler {
// Anime / Search / Catalog
mux.HandleFunc("/", animeHandler.HandleCatalog)
mux.HandleFunc("/discover", animeHandler.HandleDiscover)
+ mux.HandleFunc("/schedule", animeHandler.HandleSchedule)
+ mux.HandleFunc("/notifications", animeHandler.HandleNotifications)
+ mux.HandleFunc("/api/schedule", animeHandler.HandleAPISchedule)
mux.HandleFunc("/api/discover/airing", animeHandler.HandleAPIDiscoverAiring)
mux.HandleFunc("/api/discover/upcoming", animeHandler.HandleAPIDiscoverUpcoming)
mux.HandleFunc("/search", animeHandler.HandleSearch)
@@ -42,7 +45,7 @@ func NewRouter(cfg Config) http.Handler {
mux.HandleFunc("/api/search-quick", animeHandler.HandleQuickSearch)
mux.HandleFunc("/api/catalog", animeHandler.HandleAPICatalog)
mux.HandleFunc("/anime/", animeHandler.HandleAnimeDetails)
- mux.HandleFunc("/api/anime/", animeHandler.HandleAPIAnimeRelations)
+ mux.HandleFunc("/api/anime/", animeHandler.HandleAPIAnime)
// Auth Endpoints
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/templates/anime.templ b/internal/templates/anime.templ
index 9cc63d0..ae23d65 100644
--- a/internal/templates/anime.templ
+++ b/internal/templates/anime.templ
@@ -56,6 +56,14 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) {
loading relations
+
+
+
+
+
+
loading recommendations
+
+