Yōten: A social tracker for your language learning journey built on the atproto.

feat: add notification pages / partials

brookjeynes.dev 9c8f8954 7a892a74

verified
+139 -22
+40
internal/server/views/notifications.templ
··· 1 + package views 2 + 3 + import ( 4 + "yoten.app/internal/server/views/layouts" 5 + "yoten.app/internal/server/views/partials" 6 + ) 7 + 8 + templ NotificationsPage(params NotificationsPageParams) { 9 + @layouts.Base(layouts.BaseParams{Title: "notifications"}) { 10 + @partials.Header(partials.HeaderProps{User: params.User}) 11 + <div class="container mx-auto px-4 py-8 max-w-4xl"> 12 + <div class="flex items-center justify-between mb-8"> 13 + <div> 14 + <h1 class="text-3xl font-bold">Notifications</h1> 15 + <p class="text-text-muted mt-1">Stay updated with your learning journey</p> 16 + </div> 17 + <div class="flex items-center space-x-2"> 18 + if params.User.UnreadNotificationCount > 0 { 19 + <button 20 + class="btn btn-muted" 21 + type="button" 22 + id="mark-all-button" 23 + hx-disabled-elt="#mark-all-button" 24 + hx-post="/notification/mark-all-read" 25 + > 26 + <i class="w-4 h-4" data-lucide="check"></i> 27 + <span>Mark all read</span> 28 + <i class="w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" data-lucide="loader-circle"></i> 29 + </button> 30 + } 31 + </div> 32 + </div> 33 + <div id="study-feed" hx-trigger="load" hx-get="/notification/feed" class="flex flex-col gap-6"> 34 + <div class="flex justify-center py-4"> 35 + <i data-lucide="loader-circle" class="w-6 h-6 animate-spin text-text-muted"></i> 36 + </div> 37 + </div> 38 + </div> 39 + } 40 + }
+13 -22
internal/server/views/partials/header.templ
··· 8 8 <a class="text-xl font-bold" href="/">Yōten <span class="text-sm font-normal italic">alpha</span></a> 9 9 if params.User != nil { 10 10 <div class="flex gap-4"> 11 - <div class="flex gap-2 items-center"> 12 - <a 13 - href="/profile/activities" 14 - class="text-sm hover:underline" 15 - > 11 + <nav class="flex gap-2 items-center"> 12 + <a href="/profile/activities" class="text-sm hover:underline"> 16 13 Activities 17 14 </a> 18 - <a 19 - href="/profile/resources" 20 - class="text-sm hover:underline" 21 - > 15 + <a href="/profile/resources" class="text-sm hover:underline"> 22 16 Resources 23 17 </a> 24 - <a 25 - href="/stats" 26 - class="text-sm hover:underline" 27 - > 18 + <a href="/stats" class="text-sm hover:underline"> 28 19 Stats 29 20 </a> 30 - </div> 21 + <a href="/profile/notifications" class="relative"> 22 + <i class="w-5 h-5" data-lucide="bell"></i> 23 + if params.User.UnreadNotificationCount > 0 { 24 + <span class="absolute top-0 right-0 block h-2 w-2 rounded-full bg-red-500"></span> 25 + } 26 + </a> 27 + </nav> 31 28 <details class="relative inline-block text-left"> 32 29 <summary class="cursor-pointer list-none"> 33 30 if params.User.BskyProfile.Avatar == "" { ··· 48 45 <i class="w-4 h-4" data-lucide="user"></i> 49 46 Profile 50 47 </a> 51 - <a 52 - href="/friends" 53 - class="flex items-center px-4 py-2 text-sm hover:bg-bg gap-2" 54 - > 48 + <a href="/friends" class="flex items-center px-4 py-2 text-sm hover:bg-bg gap-2"> 55 49 <i class="w-4 h-4" data-lucide="users"></i> 56 50 Friends 57 51 </a> 58 - <a 59 - href="/profile/edit" 60 - class="flex items-center px-4 py-2 text-sm hover:bg-bg gap-2" 61 - > 52 + <a href="/profile/edit" class="flex items-center px-4 py-2 text-sm hover:bg-bg gap-2"> 62 53 <i class="w-4 h-4" data-lucide="settings"></i> 63 54 Settings 64 55 </a>
+35
internal/server/views/partials/notification-feed.templ
··· 1 + package partials 2 + 3 + import "fmt" 4 + 5 + templ NotificationFeed(params NotificationFeedProps) { 6 + <div x-init="lucide.createIcons()"> 7 + <div id="notification-feed" class="flex flex-col gap-6"> 8 + for _, notification := range params.Feed { 9 + @Notification(NotificationProps{ 10 + Notification: notification, 11 + }) 12 + } 13 + if params.NextPage > 0 { 14 + <div 15 + id="next-notification-segment" 16 + hx-get={ templ.SafeURL(fmt.Sprintf("/notification/feed?page=%d", params.NextPage)) } 17 + hx-trigger="revealed" 18 + hx-swap="outerHTML" 19 + > 20 + <div class="flex justify-center py-4"> 21 + <i data-lucide="loader-circle" class="w-6 h-6 animate-spin text-text-muted"></i> 22 + </div> 23 + </div> 24 + } 25 + </div> 26 + if len(params.Feed) == 0 { 27 + <div class="flex flex-col gap-6"> 28 + <div class="text-center py-12"> 29 + <p class="text-text text-lg">All caught up!</p> 30 + <p class="text-text-muted">You have no new notifications.</p> 31 + </div> 32 + </div> 33 + } 34 + </div> 35 + }
+33
internal/server/views/partials/notification.templ
··· 1 + package partials 2 + 3 + import "yoten.app/internal/db" 4 + 5 + templ Notification(params NotificationProps) { 6 + <div 7 + id={ params.Notification.ID } 8 + class={ "card", templ.KV("border border-primary", params.Notification.State==db.NotificationStateUnread) } 9 + > 10 + switch (params.Notification.Type) { 11 + case db.NotificationTypeFollow: 12 + <div> 13 + <h1 class="font-semibold">New Follower</h1> 14 + <p class="text-sm mt-1"> 15 + <a class="hover:underline" href={ templ.SafeURL("/" + params.Notification.ActorDid) }> 16 + { params.Notification.ActorDid } 17 + </a> started following you 18 + </p> 19 + </div> 20 + case db.NotificationTypeReaction: 21 + <div> 22 + <h1 class="font-semibold">New Reaction</h1> 23 + <p class="text-sm mt-1"> 24 + <a class="hover:underline" href={ templ.SafeURL("/" + params.Notification.ActorDid) }> 25 + &commat;{ params.Notification.ActorBskyHandle } 26 + </a> reacted to your study session 27 + </p> 28 + </div> 29 + default: 30 + } 31 + <p class="text-xs text-text-muted">{ params.Notification.CreatedAt.Format("02/01/2006") }</p> 32 + </div> 33 + }
+11
internal/server/views/partials/partials.go
··· 207 207 Title string 208 208 Segments []db.DonutChartSegment 209 209 } 210 + 211 + type NotificationProps struct { 212 + Notification db.NotificationWithBskyHandle 213 + } 214 + 215 + type NotificationFeedProps struct { 216 + // The current logged in user 217 + User *types.User 218 + Feed []db.NotificationWithBskyHandle 219 + NextPage int 220 + }
+7
internal/server/views/views.go
··· 118 118 InputOutputPercentage int 119 119 LanguageSummaryChartSegments []db.DonutChartSegment 120 120 } 121 + 122 + type NotificationsPageParams struct { 123 + // The current logged in user. 124 + User *types.User 125 + Notifications []db.NotificationWithBskyHandle 126 + ActiveTab string 127 + }