A social RSS reader built on the AT Protocol. glean.at
glean atproto atmosphere rss feed social app
14
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve logo and refactor pagination, add confirm dialog

+205 -148
+37
internal/server/annotations_handler.go
··· 4 4 "database/sql" 5 5 "fmt" 6 6 "net/http" 7 + "strconv" 7 8 "time" 8 9 9 10 "pkg.rbrt.fr/glean/internal/atproto" 10 11 "pkg.rbrt.fr/glean/internal/db" 11 12 ) 13 + 14 + func (s *Server) handleLibrary(w http.ResponseWriter, r *http.Request) { 15 + user := currentUser(r) 16 + 17 + likedOffset, _ := strconv.Atoi(r.URL.Query().Get("liked_offset")) 18 + if likedOffset < 0 { 19 + likedOffset = 0 20 + } 21 + annotOffset, _ := strconv.Atoi(r.URL.Query().Get("annot_offset")) 22 + if annotOffset < 0 { 23 + annotOffset = 0 24 + } 25 + limit := 20 26 + 27 + articles, _ := s.db.ListLikedArticles(r.Context(), user.DID, limit+1, likedOffset) 28 + likedHasMore := len(articles) > limit 29 + if likedHasMore { 30 + articles = articles[:limit] 31 + } 32 + 33 + annotations, _ := s.db.ListAnnotations(r.Context(), "", "", user.DID, limit+1, annotOffset) 34 + annotHasMore := len(annotations) > limit 35 + if annotHasMore { 36 + annotations = annotations[:limit] 37 + } 38 + 39 + s.render(w, r, "library.html", map[string]any{ 40 + "User": user, 41 + "Articles": articles, 42 + "Annotations": annotations, 43 + "LikedHasMore": likedHasMore, 44 + "AnnotHasMore": annotHasMore, 45 + "NextLiked": likedOffset + limit, 46 + "NextAnnot": annotOffset + limit, 47 + }) 48 + } 12 49 13 50 func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) { 14 51 user := currentUser(r)
+9 -15
internal/server/articles_handler.go
··· 37 37 user := currentUser(r) 38 38 feedURL := r.URL.Query().Get("feed") 39 39 40 - offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) 41 - if offset < 0 { 42 - offset = 0 43 - } 44 - pageLimit := 50 45 - 46 - articles, err := s.db.ListArticles(r.Context(), user.DID, feedURL, pageLimit+1, offset) 40 + page := pageFromRequest(r, 50) 41 + articles, err := s.db.ListArticles(r.Context(), user.DID, feedURL, page.FetchLimit(), page.Offset) 47 42 if err != nil { 48 43 s.logger.Error("failed to list articles", "error", err) 49 44 http.Error(w, err.Error(), http.StatusInternalServerError) 50 45 return 51 46 } 52 47 53 - hasMore := len(articles) > pageLimit 54 - if hasMore { 55 - articles = articles[:pageLimit] 48 + page = page.Paginate(len(articles)) 49 + if page.HasMore { 50 + articles = articles[:page.Limit] 56 51 } 57 52 58 53 s.render(w, r, "articles.html", map[string]any{ 59 - "User": user, 60 - "Articles": articles, 61 - "FeedURL": feedURL, 62 - "HasMore": hasMore, 63 - "NextOffset": offset + pageLimit, 54 + "User": user, 55 + "Articles": articles, 56 + "FeedURL": feedURL, 57 + "Page": page, 64 58 }) 65 59 } 66 60
+13 -20
internal/server/dashboard_handler.go
··· 2 2 3 3 import ( 4 4 "net/http" 5 - "strconv" 6 5 "time" 7 6 ) 8 7 ··· 12 11 unreadCount, _ := s.db.GetUnreadCount(r.Context(), user.DID, "") 13 12 subCount, _ := s.db.GetSubscriptionCount(r.Context(), user.DID) 14 13 15 - offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) 16 - if offset < 0 { 17 - offset = 0 18 - } 19 - pageLimit := 25 20 - 21 - articles, _ := s.db.ListUnreadArticles(r.Context(), user.DID, "", pageLimit+1, offset) 22 - hasMore := len(articles) > pageLimit 23 - if hasMore { 24 - articles = articles[:pageLimit] 14 + page := pageFromRequest(r, 25) 15 + articles, _ := s.db.ListUnreadArticles(r.Context(), user.DID, "", page.FetchLimit(), page.Offset) 16 + page = page.Paginate(len(articles)) 17 + if page.HasMore { 18 + articles = articles[:page.Limit] 25 19 } 26 20 27 21 articleRecs, _ := s.db.GetArticleRecommendations(r.Context(), user.DID, 5) ··· 32 26 personalTrending, _ := s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, 5, 0) 33 27 34 28 s.render(w, r, "dashboard.html", map[string]any{ 35 - "User": user, 36 - "UnreadCount": unreadCount, 37 - "SubscriptionCount": subCount, 38 - "Articles": articles, 29 + "User": user, 30 + "UnreadCount": unreadCount, 31 + "SubscriptionCount": subCount, 32 + "Articles": articles, 39 33 "ArticleRecommendations": articleRecs, 40 - "FeedRecommendations": feedRecs, 41 - "PeopleRecommendations": peopleRecs, 42 - "PersonalTrending": personalTrending, 43 - "HasMore": hasMore, 44 - "NextOffset": offset + pageLimit, 34 + "FeedRecommendations": feedRecs, 35 + "PeopleRecommendations": peopleRecs, 36 + "PersonalTrending": personalTrending, 37 + "Page": page, 45 38 }) 46 39 }
+6 -12
internal/server/feeds_handler.go
··· 18 18 user := currentUser(r) 19 19 category := r.URL.Query().Get("category") 20 20 21 - offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) 22 - if offset < 0 { 23 - offset = 0 24 - } 25 - pageLimit := 50 26 - 27 - subs, _ := s.db.ListSubscriptions(r.Context(), user.DID, category, pageLimit+1, offset) 28 - hasMore := len(subs) > pageLimit 29 - if hasMore { 30 - subs = subs[:pageLimit] 21 + page := pageFromRequest(r, 50) 22 + subs, _ := s.db.ListSubscriptions(r.Context(), user.DID, category, page.FetchLimit(), page.Offset) 23 + page = page.Paginate(len(subs)) 24 + if page.HasMore { 25 + subs = subs[:page.Limit] 31 26 } 32 27 33 28 allSubs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 1000, 0) ··· 52 47 "FeedRecommendations": feedRecs, 53 48 "PeopleRecommendations": peopleRecs, 54 49 "DeadFeeds": deadFeeds, 55 - "HasMore": hasMore, 56 - "NextOffset": offset + pageLimit, 50 + "Page": page, 57 51 }) 58 52 } 59 53
-44
internal/server/library_handler.go
··· 1 - package server 2 - 3 - import ( 4 - "net/http" 5 - "strconv" 6 - ) 7 - 8 - func (s *Server) handleLibrary(w http.ResponseWriter, r *http.Request) { 9 - user := currentUser(r) 10 - 11 - likedOffset, _ := strconv.Atoi(r.URL.Query().Get("liked_offset")) 12 - if likedOffset < 0 { 13 - likedOffset = 0 14 - } 15 - annotOffset, _ := strconv.Atoi(r.URL.Query().Get("annot_offset")) 16 - if annotOffset < 0 { 17 - annotOffset = 0 18 - } 19 - pageLimit := 20 20 - 21 - articles, _ := s.db.ListLikedArticles(r.Context(), user.DID, pageLimit+1, likedOffset) 22 - likedHasMore := len(articles) > pageLimit 23 - if likedHasMore { 24 - articles = articles[:pageLimit] 25 - } 26 - 27 - annotations, _ := s.db.ListAnnotations(r.Context(), "", "", user.DID, pageLimit+1, annotOffset) 28 - annotHasMore := len(annotations) > pageLimit 29 - if annotHasMore { 30 - annotations = annotations[:pageLimit] 31 - } 32 - 33 - s.render(w, r, "library.html", map[string]any{ 34 - "User": user, 35 - "Articles": articles, 36 - "Annotations": annotations, 37 - "LikedHasMore": likedHasMore, 38 - "AnnotHasMore": annotHasMore, 39 - "LikedOffset": likedOffset, 40 - "AnnotOffset": annotOffset, 41 - "NextLiked": likedOffset + pageLimit, 42 - "NextAnnot": annotOffset + pageLimit, 43 - }) 44 - }
+36
internal/server/pagination.go
··· 1 + package server 2 + 3 + import ( 4 + "net/http" 5 + "strconv" 6 + ) 7 + 8 + const defaultPageSize = 25 9 + 10 + type Pagination struct { 11 + Offset int 12 + Limit int 13 + HasMore bool 14 + NextOffset int 15 + } 16 + 17 + func pageFromRequest(r *http.Request, limit int) Pagination { 18 + if limit <= 0 { 19 + limit = defaultPageSize 20 + } 21 + offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) 22 + if offset < 0 { 23 + offset = 0 24 + } 25 + return Pagination{Offset: offset, Limit: limit} 26 + } 27 + 28 + func (p Pagination) Paginate(count int) Pagination { 29 + p.HasMore = count > p.Limit 30 + p.NextOffset = p.Offset + p.Limit 31 + return p 32 + } 33 + 34 + func (p Pagination) FetchLimit() int { 35 + return p.Limit + 1 36 + }
+10 -17
internal/server/trending_handler.go
··· 2 2 3 3 import ( 4 4 "net/http" 5 - "strconv" 6 5 "time" 7 6 8 7 "pkg.rbrt.fr/glean/internal/db" ··· 16 15 scope = "for-me" 17 16 } 18 17 19 - offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) 20 - if offset < 0 { 21 - offset = 0 22 - } 23 - pageLimit := 25 24 - 18 + page := pageFromRequest(r, 25) 25 19 since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339) 26 20 27 21 var trending []*db.TrendingItem 28 22 if scope == "for-me" { 29 - trending, _ = s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, pageLimit+1, offset) 23 + trending, _ = s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, page.FetchLimit(), page.Offset) 30 24 } else { 31 - trending, _ = s.db.ListTrendingArticles(r.Context(), since, pageLimit+1, offset) 25 + trending, _ = s.db.ListTrendingArticles(r.Context(), since, page.FetchLimit(), page.Offset) 32 26 } 33 27 34 - hasMore := len(trending) > pageLimit 35 - if hasMore { 36 - trending = trending[:pageLimit] 28 + page = page.Paginate(len(trending)) 29 + if page.HasMore { 30 + trending = trending[:page.Limit] 37 31 } 38 32 39 33 s.render(w, r, "trending.html", map[string]any{ 40 - "User": user, 41 - "Trending": trending, 42 - "Scope": scope, 43 - "HasMore": hasMore, 44 - "NextOffset": offset + pageLimit, 34 + "User": user, 35 + "Trending": trending, 36 + "Scope": scope, 37 + "Page": page, 45 38 }) 46 39 }
+2 -2
internal/tmpl/articles.html
··· 17 17 {{end}} 18 18 </div> 19 19 20 - {{if .HasMore}} 20 + {{if .Page.HasMore}} 21 21 <div class="text-center py-4"> 22 - <button hx-get="/articles?offset={{.NextOffset}}{{if .FeedURL}}&feed={{.FeedURL}}{{end}}{{if .Starred}}&starred=1{{end}}" 22 + <button hx-get="/articles?offset={{.Page.NextOffset}}{{if .FeedURL}}&feed={{.FeedURL}}{{end}}" 23 23 hx-target="#article-list" hx-swap="beforeend" 24 24 hx-select="article" 25 25 class="text-sm text-spot-secondary hover:text-spot-green transition">
+43 -13
internal/tmpl/base.html
··· 10 10 </script> 11 11 <link rel="stylesheet" href="/static/output.css"> 12 12 <script src="https://unpkg.com/htmx.org@2"></script> 13 + <script> 14 + document.addEventListener('htmx:confirm', function(e) { 15 + if (!e.detail.question) return; 16 + e.preventDefault(); 17 + var dlg = document.getElementById('confirm-dialog'); 18 + document.getElementById('confirm-dialog-msg').textContent = e.detail.question; 19 + var okBtn = document.getElementById('confirm-dialog-ok'); 20 + var handler = function() { 21 + dlg.close(); 22 + okBtn.removeEventListener('click', handler); 23 + e.detail.issueRequest(true); 24 + }; 25 + okBtn.addEventListener('click', handler); 26 + dlg.showModal(); 27 + }); 28 + </script> 13 29 <link rel="icon" type="image/svg+xml" href="/static/favicon.svg"> 14 30 <link rel="preconnect" href="https://fonts.googleapis.com"> 15 31 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 16 32 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> 17 33 </head> 18 34 <body class="bg-spot-bg text-spot-text min-h-screen flex"> 35 + <dialog id="confirm-dialog" class="bg-spot-surface rounded-xl p-6 max-w-sm shadow-lg border border-spot-divider backdrop:bg-black/50" style="color-scheme:dark" onclick="if(event.target===this)this.close()"> 36 + <p id="confirm-dialog-msg" class="text-spot-text text-sm mb-6"></p> 37 + <div class="flex justify-end gap-3"> 38 + <button onclick="document.getElementById('confirm-dialog').close(false)" class="text-sm text-spot-secondary hover:text-spot-text px-4 py-2 rounded-pill border border-spot-outline transition">Cancel</button> 39 + <button id="confirm-dialog-ok" class="text-sm text-white bg-spot-red hover:brightness-110 px-4 py-2 rounded-pill font-bold uppercase tracking-button transition">Confirm</button> 40 + </div> 41 + </dialog> 19 42 {{if .User}} 20 43 <aside class="hidden lg:flex flex-col w-60 bg-spot-bg h-screen fixed left-0 top-0 px-3 py-4 z-20"> 21 44 <div class="mb-8 px-3"> ··· 43 66 Library 44 67 </a> 45 68 </nav> 46 - <div class="mt-auto pt-4 border-t border-spot-divider-30"> 47 - <a href="/profile/{{.User.DID}}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-spot-hover-50 transition"> 48 - {{if .User.AvatarURL.Valid}}<img src="{{.User.AvatarURL.String}}" class="w-8 h-8 rounded-full">{{end}} 49 - <div class="min-w-0 flex-1"> 50 - <div class="text-sm font-bold truncate text-spot-text">@{{.User.Handle}}</div> 51 - </div> 69 + <div class="mt-auto pt-4 border-t border-spot-divider-30 flex items-center gap-2 px-1"> 70 + <a href="/profile/{{.User.DID}}" class="flex items-center gap-2 flex-1 min-w-0 px-2 py-1.5 rounded-md hover:bg-spot-hover-50 transition"> 71 + {{if .User.AvatarURL.Valid}}<img src="{{.User.AvatarURL.String}}" class="w-7 h-7 rounded-full shrink-0">{{end}} 72 + <span class="text-sm font-medium truncate text-spot-text">@{{.User.Handle}}</span> 52 73 </a> 53 - <form method="POST" action="/auth/logout"> 74 + <form method="POST" action="/auth/logout" class="shrink-0"> 54 75 {{csrfInput .CSRFToken}} 55 - <button type="submit" class="text-spot-secondary hover:text-spot-red transition" title="Logout"> 56 - <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg> 76 + <button type="submit" class="text-spot-secondary hover:text-spot-red p-1.5 rounded-md hover:bg-spot-hover-50 transition" title="Logout"> 77 + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg> 57 78 </button> 58 79 </form> 59 80 </div> ··· 113 134 <svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg> 114 135 Trending 115 136 </a> 116 - </div> 117 - <div class="flex flex-col gap-1.5"> 118 - <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Library</div> 119 137 <a href="/articles" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition"> 120 138 <svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2"/></svg> 121 139 Articles 122 140 </a> 141 + </div> 142 + <div class="flex flex-col gap-1.5"> 143 + <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Library</div> 123 144 <a href="/feeds" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition"> 124 145 <svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 5c7.18 0 13 5.82 13 13M6 11a7 7 0 017 7m-7-1a1 1 0 11-2 0 1 1 0 012 0z"/></svg> 125 146 Feeds ··· 140 161 </div> 141 162 </div> 142 163 <div class="text-right"> 143 - <div class="text-xs text-spot-secondary">&copy; {{now.Format "2006"}} Glean</div> 164 + <div class="text-xs text-spot-secondary">&copy; {{now.Format "2006"}} Glean.at</div> 144 165 <div class="text-[10px] text-spot-muted mt-0.5">Built on the AT Protocol</div> 145 166 </div> 146 167 </div> ··· 198 219 }); 199 220 } 200 221 updateThemeIcons(document.documentElement.getAttribute('data-theme') || 'dark'); 222 + 223 + function toggleAnnotate(btn) { 224 + var card = btn.closest('article'); 225 + var form = card.querySelector('.annotate-form'); 226 + form.classList.toggle('hidden'); 227 + if (!form.classList.contains('hidden')) { 228 + form.querySelector('input[name="quote"]').focus(); 229 + } 230 + } 201 231 202 232 document.addEventListener('keydown', function(e) { 203 233 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+2 -2
internal/tmpl/dashboard.html
··· 38 38 {{end}} 39 39 </div> 40 40 41 - {{if .HasMore}} 41 + {{if .Page.HasMore}} 42 42 <div class="text-center py-4"> 43 - <button hx-get="/dashboard?offset={{.NextOffset}}" 43 + <button hx-get="/dashboard?offset={{.Page.NextOffset}}" 44 44 hx-target="#article-list" hx-swap="beforeend" 45 45 hx-select="article" 46 46 class="text-sm text-spot-secondary hover:text-spot-green transition">
+2 -2
internal/tmpl/feeds.html
··· 90 90 {{end}} 91 91 </div> 92 92 93 - {{if .HasMore}} 93 + {{if .Page.HasMore}} 94 94 <div class="text-center py-4"> 95 - <button hx-get="/feeds?offset={{.NextOffset}}{{if .Category}}&category={{.Category}}{{end}}" 95 + <button hx-get="/feeds?offset={{.Page.NextOffset}}{{if .Category}}&category={{.Category}}{{end}}" 96 96 hx-target="#feed-list" hx-swap="beforeend" 97 97 hx-select=".px-5" 98 98 class="text-sm text-spot-secondary hover:text-spot-green transition">
+1 -1
internal/tmpl/index.html
··· 139 139 </div> 140 140 <div class="flex items-center gap-2 text-[rgba(255,255,255,0.70)]"> 141 141 <svg class="w-4 h-4 text-spot-green" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> 142 - Open source 142 + <a href="https://tangled.org/julien.rbrt.fr/glean" class="hover:text-white transition">Open source</a> 143 143 </div> 144 144 <div class="flex items-center gap-2 text-[rgba(255,255,255,0.70)]"> 145 145 <svg class="w-4 h-4 text-spot-green" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
+17 -1
internal/tmpl/partials/article-card.html
··· 1 1 {{define "article-card.html"}} 2 - <article data-article-id="{{.ID}}" class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot"> 2 + <article data-article-id="{{.ID}}" class="group bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot relative"> 3 3 <div class="flex items-start justify-between gap-4"> 4 4 <div class="min-w-0 flex-1"> 5 5 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> ··· 14 14 </div> 15 15 <div class="flex flex-col items-center gap-1 shrink-0"> 16 16 {{template "like-button.html" .}} 17 + <button type="button" onclick="toggleAnnotate(this)" class="opacity-0 group-hover:opacity-100 text-spot-muted hover:text-spot-green text-xs transition mt-1" title="Annotate"> 18 + <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/></svg> 19 + </button> 17 20 </div> 21 + </div> 22 + <div class="annotate-form hidden mt-3 pt-3 border-t border-spot-divider"> 23 + <form hx-post="/library/create" hx-swap="none" hx-on::after-request="toggleAnnotate(this.closest('.annotate-form').previousElementSibling)"> 24 + <input type="hidden" name="feed_url" value="{{.FeedURL}}"> 25 + <input type="hidden" name="article_url" value="{{if .URL.Valid}}{{.URL.String}}{{end}}"> 26 + <div class="flex gap-2"> 27 + <input type="text" name="quote" placeholder="Quote a passage..." 28 + class="flex-1 bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder"> 29 + <input type="text" name="note" placeholder="Note (optional)" 30 + class="flex-1 bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder"> 31 + <button type="submit" class="bg-spot-green text-white rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:brightness-110 transition">Save</button> 32 + </div> 33 + </form> 18 34 </div> 19 35 </article> 20 36 {{end}}
+13 -8
internal/tmpl/partials/logo.html
··· 1 1 {{define "logo-icon"}}<svg class="w-full h-full" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 2 <rect width="32" height="32" rx="8" fill="#00754A"/> 3 - <path d="M16 8 L16 22" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/> 4 - <path d="M16 11 Q11 9 9 13" stroke="#fff" stroke-width="1.8" stroke-linecap="round" fill="none"/> 5 - <path d="M16 11 Q21 9 23 13" stroke="#fff" stroke-width="1.8" stroke-linecap="round" fill="none"/> 6 - <path d="M16 15 Q10 13 8 18" stroke="#fff" stroke-width="1.8" stroke-linecap="round" fill="none"/> 7 - <path d="M16 15 Q22 13 24 18" stroke="#fff" stroke-width="1.8" stroke-linecap="round" fill="none"/> 8 - <circle cx="16" cy="24" r="1.5" fill="#fff"/> 3 + <ellipse cx="16" cy="20" rx="5.5" ry="6" fill="#fff"/> 4 + <path d="M10.5 19.5c-2.5-1-5.5-4-5-7.5.2-1.2 1-1.8 1.8-1.5 2 .7 3.5 4 3.8 7" fill="#fff" fill-opacity=".5"/> 5 + <path d="M21.5 19.5c2.5-1 5.5-4 5-7.5-.2-1.2-1-1.8-1.8-1.5-2 .7-3.5 4-3.8 7" fill="#fff" fill-opacity=".5"/> 6 + <line x1="13" y1="17" x2="19" y2="17" stroke="#00754A" stroke-width="1.2"/> 7 + <line x1="12.5" y1="20" x2="19.5" y2="20" stroke="#00754A" stroke-width="1.2"/> 8 + <line x1="13" y1="23" x2="19" y2="23" stroke="#00754A" stroke-width="1.2"/> 9 + <circle cx="13.5" cy="11" r="1.8" fill="#fff"/> 10 + <circle cx="18.5" cy="11" r="1.8" fill="#fff"/> 11 + <line x1="13.5" y1="9.2" x2="12" y2="6.5" stroke="#fff" stroke-width="1.3" stroke-linecap="round"/> 12 + <line x1="18.5" y1="9.2" x2="20" y2="6.5" stroke="#fff" stroke-width="1.3" stroke-linecap="round"/> 13 + <path d="M13 26c.5 1 1.5 1.5 3 1.5s2.5-.5 3-1.5" stroke="#fff" stroke-width="1.2" stroke-linecap="round" fill="none"/> 9 14 </svg>{{end}} 10 15 11 16 {{define "logo-link"}}<a href="/" class="text-spot-green font-bold text-xl tracking-tight flex items-center gap-2"> 12 17 <span class="w-7 h-7">{{template "logo-icon"}}</span> 13 - Glean 18 + Glean.at 14 19 </a>{{end}} 15 20 16 - {{define "logo-text"}}<a href="/" class="text-spot-green font-bold text-lg tracking-tight">Glean</a>{{end}} 21 + {{define "logo-text"}}<a href="/" class="text-spot-green font-bold text-lg tracking-tight">Glean.at</a>{{end}}
+2 -2
internal/tmpl/trending.html
··· 37 37 {{end}} 38 38 </div> 39 39 40 - {{if .HasMore}} 40 + {{if .Page.HasMore}} 41 41 <div class="text-center py-4"> 42 - <button hx-get="/trending?scope={{.Scope}}&offset={{.NextOffset}}" 42 + <button hx-get="/trending?scope={{.Scope}}&offset={{.Page.NextOffset}}" 43 43 hx-target="#trending-list" hx-swap="beforeend" 44 44 hx-select="article" 45 45 class="text-sm text-spot-secondary hover:text-spot-green transition">
+1 -1
readme.md
··· 1 - # Glean 1 + # [Glean.at](https://glean.at)
+11 -8
static/favicon.svg
··· 1 1 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none"> 2 2 <rect width="32" height="32" rx="8" fill="#1E3932"/> 3 - <path d="M16 6 L16 22" stroke="#00754A" stroke-width="2.5" stroke-linecap="round"/> 4 - <path d="M16 10 Q10 8 8 12" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/> 5 - <path d="M16 10 Q22 8 24 12" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/> 6 - <path d="M16 14 Q9 12 7 17" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/> 7 - <path d="M16 14 Q23 12 25 17" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/> 8 - <path d="M16 18 Q11 16 9 21" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/> 9 - <path d="M16 18 Q21 16 23 21" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/> 10 - <circle cx="16" cy="25" r="2" fill="#00754A"/> 3 + <ellipse cx="16" cy="20" rx="5.5" ry="6" fill="#00754A"/> 4 + <path d="M10.5 19.5c-2.5-1-5.5-4-5-7.5.2-1.2 1-1.8 1.8-1.5 2 .7 3.5 4 3.8 7" fill="#00754A" fill-opacity=".5"/> 5 + <path d="M21.5 19.5c2.5-1 5.5-4 5-7.5-.2-1.2-1-1.8-1.8-1.5-2 .7-3.5 4-3.8 7" fill="#00754A" fill-opacity=".5"/> 6 + <line x1="13" y1="17" x2="19" y2="17" stroke="#1E3932" stroke-width="1.2"/> 7 + <line x1="12.5" y1="20" x2="19.5" y2="20" stroke="#1E3932" stroke-width="1.2"/> 8 + <line x1="13" y1="23" x2="19" y2="23" stroke="#1E3932" stroke-width="1.2"/> 9 + <circle cx="13.5" cy="11" r="1.8" fill="#00754A"/> 10 + <circle cx="18.5" cy="11" r="1.8" fill="#00754A"/> 11 + <line x1="13.5" y1="9.2" x2="12" y2="6.5" stroke="#00754A" stroke-width="1.3" stroke-linecap="round"/> 12 + <line x1="18.5" y1="9.2" x2="20" y2="6.5" stroke="#00754A" stroke-width="1.3" stroke-linecap="round"/> 13 + <path d="M13 26c.5 1 1.5 1.5 3 1.5s2.5-.5 3-1.5" stroke="#00754A" stroke-width="1.2" stroke-linecap="round" fill="none"/> 11 14 </svg>