feat: replace notifications with continue watching

This commit is contained in:
2026-04-18 23:42:22 +02:00
parent dea66f2f6a
commit ed73400b83
9 changed files with 137 additions and 292 deletions

View File

@@ -0,0 +1,67 @@
package templates
import (
"fmt"
"mal/internal/database"
"mal/internal/shared/ui"
)
templ ContinueWatching(entries []database.GetContinueWatchingEntriesRow) {
@Layout("mal - continue watching", true) {
<div class="grid gap-4">
<h1>Continue watching</h1>
<p class="m-0 text-sm text-(--text-muted)">Pick up where you left off.</p>
if len(entries) == 0 {
@ui.EmptyState("Nothing to continue yet") {
Start watching any anime and your progress will show up here.
}
} else {
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5">
for _, entry := range entries {
<div class="group relative min-w-0" id={ fmt.Sprintf("continue-entry-%d", entry.AnimeID) }>
@ui.AnimeCard(ui.AnimeCardProps{
ID: int(entry.AnimeID),
Title: displayContinueWatchingTitle(entry),
ImageURL: entry.ImageUrl,
Href: continueWatchingURL(entry),
Class: "notification-card min-w-0 flex flex-col bg-transparent text-inherit no-underline",
HideTitle: true,
}) {
<div class="mt-2 grid gap-1 p-0">
<div class="line-clamp-2 text-sm leading-snug text-(--text)">{ displayContinueWatchingTitle(entry) }</div>
<div class="flex flex-wrap gap-2">
if entry.CurrentEpisode.Valid && entry.CurrentEpisode.Int64 > 0 {
<span class="text-xs text-(--text-faint)">Continue ep { fmt.Sprintf("%d", entry.CurrentEpisode.Int64) }</span>
}
if entry.CurrentTimeSeconds > 0 {
<span class="text-xs text-(--text-faint)">{ formatProgressTime(entry.CurrentTimeSeconds) }</span>
}
</div>
</div>
}
<button
class="absolute right-2 top-2 h-6 w-6 cursor-pointer border-0 bg-(--overlay-subtle) text-(--text-muted) opacity-0 transition-opacity duration-150 group-hover:opacity-100 hover:text-(--danger)"
hx-delete={ string(templ.URL(fmt.Sprintf("/api/continue-watching/%d", entry.AnimeID))) }
hx-target={ fmt.Sprintf("#continue-entry-%d", entry.AnimeID) }
hx-swap="delete"
>&times;</button>
</div>
}
</div>
}
</div>
}
}
func continueWatchingURL(entry database.GetContinueWatchingEntriesRow) string {
episode := 1
if entry.CurrentEpisode.Valid && entry.CurrentEpisode.Int64 > 0 {
episode = int(entry.CurrentEpisode.Int64)
}
return fmt.Sprintf("/watch/%d/%d", entry.AnimeID, episode)
}
func displayContinueWatchingTitle(entry database.GetContinueWatchingEntriesRow) string {
return database.DisplayTitle(entry.TitleEnglish, entry.TitleJapanese, entry.TitleOriginal)
}

View File

@@ -26,9 +26,8 @@ templ Layout(title string, showHeader bool) {
@icons.LogoIcon("h-7 w-7")
</a>
<div class="flex flex-wrap gap-3 text-sm max-lg:w-full max-lg:gap-2">
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/">Catalog</a>
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/discover">Discover</a>
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/notifications">Notifications</a>
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/continue-watching">Continue watching</a>
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/watchlist">Watchlist</a>
</div>
</div>

View File

@@ -1,184 +0,0 @@
package templates
import "mal/internal/jikan"
import "mal/internal/database"
import "mal/internal/shared/ui"
import "fmt"
import "strings"
type WatchingAnimeWithDetails struct {
Entry database.GetWatchingAnimeRow
Anime jikan.Anime
}
templ Notifications(watching []WatchingAnimeWithDetails, activeTab string) {
@Layout("mal - notifications", true) {
<div class="grid gap-4">
<h1>Notifications</h1>
<div class="mb-3 flex flex-wrap gap-2 max-md:flex-nowrap max-md:overflow-x-auto max-md:pb-1">
<a href="/notifications?tab=tracking" class={ statusTabClass(activeTab == "tracking") }>Tracking</a>
<a href="/notifications?tab=sequels" class={ statusTabClass(activeTab == "sequels") }>Sequels</a>
</div>
if activeTab == "sequels" {
<div hx-get="/notifications/upcoming" hx-trigger="load, every 15s" hx-swap="innerHTML">
@ui.LoadingIndicator("Syncing sequel graphs...")
</div>
} else {
<p class="m-0 text-sm text-(--text-muted)">Shows you're currently watching or planning to watch.</p>
if len(watching) == 0 {
@ui.EmptyState("No airing anime in your watching list.") {
<span class="text-sm text-(--text-muted)">Add currently airing shows to your watching list to see upcoming episodes here.</span>
}
} else {
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5">
for _, item := range watching {
@NotificationCard(item)
}
</div>
}
}
</div>
}
}
func splitUpcomingSeasons(items []database.GetUpcomingSeasonsRow) (airing []database.GetUpcomingSeasonsRow, upcoming []database.GetUpcomingSeasonsRow) {
for _, item := range items {
if item.Status.Valid && item.Status.String == "Currently Airing" {
airing = append(airing, item)
} else {
upcoming = append(upcoming, item)
}
}
return
}
templ UpcomingSeasonsList(upcomingSeasons []database.GetUpcomingSeasonsRow) {
if len(upcomingSeasons) == 0 {
@ui.EmptyState("No upcoming seasons for anime you've watched.") {
<span class="text-sm text-(--text-muted)">As you watch more shows, new seasons will appear here.</span>
}
} else {
@renderSplitSeasons(upcomingSeasons)
}
}
templ renderSplitSeasons(upcomingSeasons []database.GetUpcomingSeasonsRow) {
if airing, upcoming := splitUpcomingSeasons(upcomingSeasons); true {
if len(airing) > 0 {
<section class="mb-4 grid gap-3">
<h2 class="m-0 text-xl font-semibold leading-tight">Airing now</h2>
<p class="m-0 text-sm text-(--text-muted)">These are the currently airing anime, but you're not tracking any of these.</p>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5">
for _, item := range airing {
@UpcomingSeasonCard(item)
}
</div>
</section>
}
if len(upcoming) > 0 {
<section class="grid gap-3">
<h2 class="m-0 text-xl font-semibold leading-tight">Announced & upcoming</h2>
<p class="m-0 text-sm text-(--text-muted)">Newly announced or upcoming seasons related to anime you've watched.</p>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5">
for _, item := range upcoming {
@UpcomingSeasonCard(item)
}
</div>
</section>
}
}
}
templ UpcomingSeasonCard(item database.GetUpcomingSeasonsRow) {
@ui.AnimeCard(ui.AnimeCardProps{
ID: int(item.ID),
Title: displaySeasonTitle(item),
ImageURL: item.ImageUrl,
Class: "notification-card min-w-0 flex flex-col bg-transparent text-inherit no-underline",
HideTitle: true,
}) {
<div class="mt-2 grid gap-1 p-0" data-notification-content>
<div class="line-clamp-2 text-sm leading-snug text-(--text)">
{ displaySeasonTitle(item) }
</div>
<div class="flex flex-wrap gap-2">
if item.Status.Valid {
<span class="text-xs text-(--text-faint)">{ seasonStatusLabel(item.Status.String) }</span>
}
if strings.TrimSpace(item.PrequelTitle) != "" {
<span class="text-xs text-(--text-faint)">{ fmt.Sprintf("Sequel to %s", item.PrequelTitle) }</span>
}
</div>
</div>
}
}
func displaySeasonTitle(entry database.GetUpcomingSeasonsRow) string {
return database.DisplayTitle(entry.TitleEnglish, entry.TitleJapanese, entry.TitleOriginal)
}
templ NotificationCard(item WatchingAnimeWithDetails) {
@ui.AnimeCard(ui.AnimeCardProps{
ID: int(item.Entry.AnimeID),
Title: displayTitle(item.Entry),
ImageURL: item.Entry.ImageUrl,
Class: "notification-card min-w-0 flex flex-col bg-transparent text-inherit no-underline",
HideTitle: true,
}) {
<div class="mt-2 grid gap-1 p-0" data-notification-content>
<div class="line-clamp-2 text-sm leading-snug text-(--text)">
{ displayTitle(item.Entry) }
</div>
<div class="flex flex-wrap gap-2">
if item.Anime.Broadcast.String != "" {
<span class="text-xs text-(--text-faint)" data-jst-text={ item.Anime.Broadcast.String } data-broadcast-day={ item.Anime.Broadcast.Day } data-broadcast-time={ item.Anime.Broadcast.Time } data-broadcast-timezone={ item.Anime.Broadcast.Timezone }>{ item.Anime.Broadcast.String }</span>
<span class="text-xs text-(--text-faint)" data-next-airing="pending">Calculating next episode time...</span>
}
if item.Anime.Episodes > 0 {
<span class="text-xs text-(--text-faint)">
if item.Entry.CurrentEpisode.Valid {
{ fmt.Sprintf("%d / %d eps", item.Entry.CurrentEpisode.Int64, item.Anime.Episodes) }
} else {
{ fmt.Sprintf("0 / %d eps", item.Anime.Episodes) }
}
</span>
} else if item.Entry.CurrentEpisode.Valid && item.Entry.CurrentEpisode.Int64 > 0 {
<span class="text-xs text-(--text-faint)">
{ fmt.Sprintf("%d eps watched", item.Entry.CurrentEpisode.Int64) }
</span>
}
</div>
</div>
}
}
func displayTitle(entry database.GetWatchingAnimeRow) string {
return database.DisplayTitle(entry.TitleEnglish, entry.TitleJapanese, entry.TitleOriginal)
}
func seasonStatusLabel(status string) string {
statusText := strings.TrimSpace(status)
if statusText == "" {
return ""
}
if statusText == "Currently Airing" {
return "Airing now"
}
if statusText == "Not yet aired" {
return "Upcoming"
}
return statusText
}
func statusTabClass(active bool) string {
base := "shrink-0 whitespace-nowrap bg-(--panel-soft) px-2 py-1 text-xs text-(--text-muted) no-underline hover:bg-(--surface-tab-hover) hover:text-(--text) hover:no-underline"
if active {
return "shrink-0 whitespace-nowrap bg-(--surface-tab-active) px-2 py-1 text-xs text-(--accent) no-underline hover:no-underline"
}
return base
}

View File

@@ -29,7 +29,6 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
<div class="mb-3 flex flex-wrap gap-2 max-md:flex-nowrap max-md:overflow-x-auto max-md:pb-1">
<a href={ templ.URL(watchlistURL(layout, "all", sortBy, sortOrder)) } class={ tabClass(currentStatus == "all") }>All</a>
<a href={ templ.URL(watchlistURL(layout, "watching", sortBy, sortOrder)) } class={ tabClass(currentStatus == "watching") }>Watching</a>
<a href={ templ.URL(watchlistURL(layout, "continuing", sortBy, sortOrder)) } class={ tabClass(currentStatus == "continuing") }>Continuing</a>
<a href={ templ.URL(watchlistURL(layout, "on_hold", sortBy, sortOrder)) } class={ tabClass(currentStatus == "on_hold") }>On hold</a>
<a href={ templ.URL(watchlistURL(layout, "plan_to_watch", sortBy, sortOrder)) } class={ tabClass(currentStatus == "plan_to_watch") }>Plan to watch</a>
<a href={ templ.URL(watchlistURL(layout, "dropped", sortBy, sortOrder)) } class={ tabClass(currentStatus == "dropped") }>Dropped</a>
@@ -40,8 +39,6 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
@ui.EmptyState("Nothing here yet") {
if currentStatus == "all" {
Your watchlist is empty. <a href="/">Search for anime</a> to get started.
} else if currentStatus == "continuing" {
No airing anime with watching or plan to watch status.
} else {
No anime in this category.
}