ui: redesign notifications grid

This commit is contained in:
2026-04-08 13:20:04 +02:00
parent 754652f46f
commit 8904df7b1c
3 changed files with 83 additions and 104 deletions

View File

@@ -32,18 +32,18 @@ templ Notifications(watching []WatchingAnimeWithDetails) {
} }
templ NotificationCard(item WatchingAnimeWithDetails) { templ NotificationCard(item WatchingAnimeWithDetails) {
<div class="notification-card"> <a href={ templ.URL(fmt.Sprintf("/anime/%d", item.Entry.AnimeID)) } class="notification-card">
<a href={ templ.URL(fmt.Sprintf("/anime/%d", item.Entry.AnimeID)) } class="notification-image"> <div class="notification-image">
if item.Entry.ImageUrl != "" { if item.Entry.ImageUrl != "" {
<img src={ item.Entry.ImageUrl } alt={ displayTitle(item.Entry) } loading="lazy"/> <img src={ item.Entry.ImageUrl } alt={ displayTitle(item.Entry) } loading="lazy"/>
} else { } else {
<div class="no-image">no image</div> <div class="no-image">no image</div>
} }
</a> </div>
<div class="notification-content"> <div class="notification-content">
<a href={ templ.URL(fmt.Sprintf("/anime/%d", item.Entry.AnimeID)) } class="notification-title"> <div class="notification-title">
{ displayTitle(item.Entry) } { displayTitle(item.Entry) }
</a> </div>
<div class="notification-meta"> <div class="notification-meta">
if item.Anime.Broadcast.String != "" { if item.Anime.Broadcast.String != "" {
<span class="notification-broadcast">{ item.Anime.Broadcast.String }</span> <span class="notification-broadcast">{ item.Anime.Broadcast.String }</span>
@@ -51,22 +51,19 @@ templ NotificationCard(item WatchingAnimeWithDetails) {
if item.Anime.Episodes > 0 { if item.Anime.Episodes > 0 {
<span class="notification-progress"> <span class="notification-progress">
if item.Entry.CurrentEpisode.Valid { if item.Entry.CurrentEpisode.Valid {
{ fmt.Sprintf("%d / %d episodes", item.Entry.CurrentEpisode.Int64, item.Anime.Episodes) } { fmt.Sprintf("%d / %d eps", item.Entry.CurrentEpisode.Int64, item.Anime.Episodes) }
} else { } else {
{ fmt.Sprintf("0 / %d episodes", item.Anime.Episodes) } { fmt.Sprintf("0 / %d eps", item.Anime.Episodes) }
} }
</span> </span>
} else if item.Entry.CurrentEpisode.Valid && item.Entry.CurrentEpisode.Int64 > 0 { } else if item.Entry.CurrentEpisode.Valid && item.Entry.CurrentEpisode.Int64 > 0 {
<span class="notification-progress"> <span class="notification-progress">
{ fmt.Sprintf("%d episodes watched", item.Entry.CurrentEpisode.Int64) } { fmt.Sprintf("%d eps watched", item.Entry.CurrentEpisode.Int64) }
</span> </span>
} }
</div> </div>
if item.Anime.Synopsis != "" {
<p class="notification-synopsis">{ truncate(item.Anime.Synopsis, 150) }</p>
}
</div> </div>
</div> </a>
} }
func displayTitle(entry database.GetWatchingAnimeRow) string { func displayTitle(entry database.GetWatchingAnimeRow) string {

View File

@@ -110,20 +110,20 @@ func NotificationCard(item WatchingAnimeWithDetails) templ.Component {
templ_7745c5c3_Var3 = templ.NopComponent templ_7745c5c3_Var3 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"notification-card\"><a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 templ.SafeURL var templ_7745c5c3_Var4 templ.SafeURL
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/anime/%d", item.Entry.AnimeID))) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/anime/%d", item.Entry.AnimeID)))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 36, Col: 67} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 35, Col: 66}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" class=\"notification-image\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" class=\"notification-card\"><div class=\"notification-image\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -164,128 +164,92 @@ func NotificationCard(item WatchingAnimeWithDetails) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</a><div class=\"notification-content\"><a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><div class=\"notification-content\"><div class=\"notification-title\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 templ.SafeURL var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/anime/%d", item.Entry.AnimeID))) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(item.Entry))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 44, Col: 68} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 45, Col: 30}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" class=\"notification-title\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div><div class=\"notification-meta\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(item.Entry))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 45, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</a><div class=\"notification-meta\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if item.Anime.Broadcast.String != "" { if item.Anime.Broadcast.String != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<span class=\"notification-broadcast\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span class=\"notification-broadcast\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.Anime.Broadcast.String) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Anime.Broadcast.String)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 49, Col: 71} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 49, Col: 71}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
if item.Anime.Episodes > 0 { if item.Anime.Episodes > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"notification-progress\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<span class=\"notification-progress\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if item.Entry.CurrentEpisode.Valid { if item.Entry.CurrentEpisode.Valid {
var templ_7745c5c3_Var10 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d episodes", item.Entry.CurrentEpisode.Int64, item.Anime.Episodes)) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d eps", item.Entry.CurrentEpisode.Int64, item.Anime.Episodes))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 54, Col: 94} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 54, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("0 / %d eps", item.Anime.Episodes))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 56, Col: 55}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else {
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("0 / %d episodes", item.Anime.Episodes))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 56, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else if item.Entry.CurrentEpisode.Valid && item.Entry.CurrentEpisode.Int64 > 0 { } else if item.Entry.CurrentEpisode.Valid && item.Entry.CurrentEpisode.Int64 > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"notification-progress\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"notification-progress\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var12 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d episodes watched", item.Entry.CurrentEpisode.Int64)) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d eps watched", item.Entry.CurrentEpisode.Int64))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 61, Col: 75} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 61, Col: 70}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div></div></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.Anime.Synopsis != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p class=\"notification-synopsis\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(truncate(item.Anime.Synopsis, 150))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/notifications.templ`, Line: 66, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -1172,7 +1172,7 @@ a:visited {
/* Notifications Page */ /* Notifications Page */
.notifications-page { .notifications-page {
max-width: 900px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
padding: var(--space-2xl) var(--space-xl); padding: var(--space-2xl) var(--space-xl);
} }
@@ -1201,49 +1201,57 @@ a:visited {
} }
.notifications-list { .notifications-list {
display: flex; display: grid;
flex-direction: column; grid-template-columns: repeat(auto-fill, minmax(clamp(140px, 12vw + 80px, 200px), 1fr));
gap: var(--space-lg); gap: var(--space-lg);
} }
.notification-card { .notification-card {
display: flex;
gap: var(--space-lg);
background: var(--surface); background: var(--surface);
border: 1px solid var(--border); border: 1px solid var(--border);
padding: var(--space-lg);
border-radius: 4px; border-radius: 4px;
transition: border-color 0.15s; overflow: hidden;
transition: border-color 0.15s, transform 0.15s;
display: flex;
flex-direction: column;
} }
.notification-card:hover { .notification-card:hover {
border-color: var(--link); border-color: var(--link);
transform: translateY(-2px);
} }
.notification-image { .notification-image {
flex-shrink: 0; aspect-ratio: 2/3;
width: var(--thumb-width); background: var(--bg);
overflow: hidden;
display: block;
} }
.notification-image img { .notification-image img {
width: 100%; width: 100%;
aspect-ratio: 2/3; height: 100%;
object-fit: cover; object-fit: cover;
border-radius: 4px;
} }
.notification-content { .notification-content {
padding: var(--space-md);
flex: 1; flex: 1;
min-width: 0; display: flex;
flex-direction: column;
} }
.notification-title { .notification-title {
font-size: var(--text-md); font-size: var(--text-sm);
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
text-decoration: none; text-decoration: none;
display: block;
margin-bottom: var(--space-xs); margin-bottom: var(--space-xs);
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
} }
.notification-title:hover { .notification-title:hover {
@@ -1253,21 +1261,31 @@ a:visited {
.notification-meta { .notification-meta {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--space-sm); gap: var(--space-xs);
font-size: var(--text-sm); font-size: var(--text-xs);
color: var(--text-muted); color: var(--text-muted);
margin-bottom: var(--space-md); margin-top: auto;
}
.notification-meta span {
background: var(--surface-hover);
padding: 2px 6px;
border-radius: 3px;
} }
.notification-broadcast { .notification-broadcast {
color: var(--link); color: var(--link) !important;
font-weight: 600; font-weight: 600;
} }
.notification-synopsis { .notification-synopsis {
font-size: var(--text-sm); font-size: var(--text-xs);
line-height: 1.5; line-height: 1.4;
color: var(--text-muted); color: var(--text-muted);
margin: 0; margin: var(--space-xs) 0 0 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
} }