nandi/gleanpublic⑂ Fork 0
⑂ 10cff72
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Improve logo and refactor pagination, add confirm dialogUnverified

Julien Robert committed 2026-04-20T16:26:39+02:00 Browse files
10cff72 parent: e9f75c1
modified internal/server/annotations_handler.go +37 -0
@@ -4,12 +4,49 @@ import (
44 "database/sql"
55 "fmt"
66 "net/http"
7+ "strconv"
78 "time"
89
910 "pkg.rbrt.fr/glean/internal/atproto"
1011 "pkg.rbrt.fr/glean/internal/db"
1112 )
1213
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+}
49+
1350 func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) {
1451 user := currentUser(r)
1552 a := &db.Annotation{
@@ -4,12 +4,49 @@ import (
4 "database/sql"4 "database/sql"
5 "fmt"5 "fmt"
6 "net/http"6 "net/http"
7+ "strconv"
7 "time"8 "time"
8 9
9 "pkg.rbrt.fr/glean/internal/atproto"10 "pkg.rbrt.fr/glean/internal/atproto"
10 "pkg.rbrt.fr/glean/internal/db"11 "pkg.rbrt.fr/glean/internal/db"
11 )12 )
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+}
49+
13 func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) {50 func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) {
14 user := currentUser(r)51 user := currentUser(r)
15 a := &db.Annotation{52 a := &db.Annotation{
modified internal/server/articles_handler.go +9 -15
@@ -37,30 +37,24 @@ func (s *Server) handleArticles(w http.ResponseWriter, r *http.Request) {
3737 user := currentUser(r)
3838 feedURL := r.URL.Query().Get("feed")
3939
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)
4742 if err != nil {
4843 s.logger.Error("failed to list articles", "error", err)
4944 http.Error(w, err.Error(), http.StatusInternalServerError)
5045 return
5146 }
5247
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]
5651 }
5752
5853 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,
6458 })
6559 }
6660
@@ -37,30 +37,24 @@ func (s *Server) handleArticles(w http.ResponseWriter, r *http.Request) {
37 user := currentUser(r)37 user := currentUser(r)
38 feedURL := r.URL.Query().Get("feed")38 feedURL := r.URL.Query().Get("feed")
39 39
40- offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))40+ page := pageFromRequest(r, 50)
41- if offset < 0 {41+ articles, err := s.db.ListArticles(r.Context(), user.DID, feedURL, page.FetchLimit(), page.Offset)
42- offset = 0
43- }
44- pageLimit := 50
45-
46- articles, err := s.db.ListArticles(r.Context(), user.DID, feedURL, pageLimit+1, offset)
47 if err != nil {42 if err != nil {
48 s.logger.Error("failed to list articles", "error", err)43 s.logger.Error("failed to list articles", "error", err)
49 http.Error(w, err.Error(), http.StatusInternalServerError)44 http.Error(w, err.Error(), http.StatusInternalServerError)
50 return45 return
51 }46 }
52 47
53- hasMore := len(articles) > pageLimit48+ page = page.Paginate(len(articles))
54- if hasMore {49+ if page.HasMore {
55- articles = articles[:pageLimit]50+ articles = articles[:page.Limit]
56 }51 }
57 52
58 s.render(w, r, "articles.html", map[string]any{53 s.render(w, r, "articles.html", map[string]any{
59- "User": user,54+ "User": user,
60- "Articles": articles,55+ "Articles": articles,
61- "FeedURL": feedURL,56+ "FeedURL": feedURL,
62- "HasMore": hasMore,57+ "Page": page,
63- "NextOffset": offset + pageLimit,
64 })58 })
65 }59 }
66 60
modified internal/server/dashboard_handler.go +13 -20
@@ -2,7 +2,6 @@ package server
22
33 import (
44 "net/http"
5- "strconv"
65 "time"
76 )
87
@@ -12,16 +11,11 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
1211 unreadCount, _ := s.db.GetUnreadCount(r.Context(), user.DID, "")
1312 subCount, _ := s.db.GetSubscriptionCount(r.Context(), user.DID)
1413
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]
2519 }
2620
2721 articleRecs, _ := s.db.GetArticleRecommendations(r.Context(), user.DID, 5)
@@ -32,15 +26,14 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
3226 personalTrending, _ := s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, 5, 0)
3327
3428 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,
3933 "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,
4538 })
4639 }
@@ -2,7 +2,6 @@ package server
2 2
3 import (3 import (
4 "net/http"4 "net/http"
5- "strconv"
6 "time"5 "time"
7 )6 )
8 7
@@ -12,16 +11,11 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
12 unreadCount, _ := s.db.GetUnreadCount(r.Context(), user.DID, "")11 unreadCount, _ := s.db.GetUnreadCount(r.Context(), user.DID, "")
13 subCount, _ := s.db.GetSubscriptionCount(r.Context(), user.DID)12 subCount, _ := s.db.GetSubscriptionCount(r.Context(), user.DID)
14 13
15- offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))14+ page := pageFromRequest(r, 25)
16- if offset < 0 {15+ articles, _ := s.db.ListUnreadArticles(r.Context(), user.DID, "", page.FetchLimit(), page.Offset)
17- offset = 016+ page = page.Paginate(len(articles))
18- }17+ if page.HasMore {
19- pageLimit := 2518+ articles = articles[:page.Limit]
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]
25 }19 }
26 20
27 articleRecs, _ := s.db.GetArticleRecommendations(r.Context(), user.DID, 5)21 articleRecs, _ := s.db.GetArticleRecommendations(r.Context(), user.DID, 5)
@@ -32,15 +26,14 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
32 personalTrending, _ := s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, 5, 0)26 personalTrending, _ := s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, 5, 0)
33 27
34 s.render(w, r, "dashboard.html", map[string]any{28 s.render(w, r, "dashboard.html", map[string]any{
35- "User": user,29+ "User": user,
36- "UnreadCount": unreadCount,30+ "UnreadCount": unreadCount,
37- "SubscriptionCount": subCount,31+ "SubscriptionCount": subCount,
38- "Articles": articles,32+ "Articles": articles,
39 "ArticleRecommendations": articleRecs,33 "ArticleRecommendations": articleRecs,
40- "FeedRecommendations": feedRecs,34+ "FeedRecommendations": feedRecs,
41- "PeopleRecommendations": peopleRecs,35+ "PeopleRecommendations": peopleRecs,
42- "PersonalTrending": personalTrending,36+ "PersonalTrending": personalTrending,
43- "HasMore": hasMore,37+ "Page": page,
44- "NextOffset": offset + pageLimit,
45 })38 })
46 }39 }
modified internal/server/feeds_handler.go +6 -12
@@ -18,16 +18,11 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
1818 user := currentUser(r)
1919 category := r.URL.Query().Get("category")
2020
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]
3126 }
3227
3328 allSubs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 1000, 0)
@@ -52,8 +47,7 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
5247 "FeedRecommendations": feedRecs,
5348 "PeopleRecommendations": peopleRecs,
5449 "DeadFeeds": deadFeeds,
55- "HasMore": hasMore,
56- "NextOffset": offset + pageLimit,
50+ "Page": page,
5751 })
5852 }
5953
@@ -18,16 +18,11 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
18 user := currentUser(r)18 user := currentUser(r)
19 category := r.URL.Query().Get("category")19 category := r.URL.Query().Get("category")
20 20
21- offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))21+ page := pageFromRequest(r, 50)
22- if offset < 0 {22+ subs, _ := s.db.ListSubscriptions(r.Context(), user.DID, category, page.FetchLimit(), page.Offset)
23- offset = 023+ page = page.Paginate(len(subs))
24- }24+ if page.HasMore {
25- pageLimit := 5025+ subs = subs[:page.Limit]
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]
31 }26 }
32 27
33 allSubs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 1000, 0)28 allSubs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 1000, 0)
@@ -52,8 +47,7 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
52 "FeedRecommendations": feedRecs,47 "FeedRecommendations": feedRecs,
53 "PeopleRecommendations": peopleRecs,48 "PeopleRecommendations": peopleRecs,
54 "DeadFeeds": deadFeeds,49 "DeadFeeds": deadFeeds,
55- "HasMore": hasMore,50+ "Page": page,
56- "NextOffset": offset + pageLimit,
57 })51 })
58 }52 }
59 53
deleted internal/server/library_handler.go +0 -44
deleted file mode 100644
@@ -1,44 +0,0 @@
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-}
deleted file mode 100644
@@ -1,44 +0,0 @@
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-}
added internal/server/pagination.go +36 -0
new file mode 100644
@@ -0,0 +1,36 @@
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+}
new file mode 100644
@@ -0,0 +1,36 @@
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+}
modified internal/tmpl/articles.html +2 -2
@@ -17,9 +17,9 @@
1717 {{end}}
1818 </div>
1919
20-{{if .HasMore}}
20+{{if .Page.HasMore}}
2121 <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}}"
2323 hx-target="#article-list" hx-swap="beforeend"
2424 hx-select="article"
2525 class="text-sm text-spot-secondary hover:text-spot-green transition">
@@ -17,9 +17,9 @@
17 {{end}}17 {{end}}
18 </div>18 </div>
19 19
20-{{if .HasMore}}20+{{if .Page.HasMore}}
21 <div class="text-center py-4">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 hx-target="#article-list" hx-swap="beforeend"23 hx-target="#article-list" hx-swap="beforeend"
24 hx-select="article"24 hx-select="article"
25 class="text-sm text-spot-secondary hover:text-spot-green transition">25 class="text-sm text-spot-secondary hover:text-spot-green transition">
modified internal/tmpl/base.html +43 -13
@@ -10,12 +10,35 @@
1010 </script>
1111 <link rel="stylesheet" href="/static/output.css">
1212 <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>
1329 <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
1430 <link rel="preconnect" href="https://fonts.googleapis.com">
1531 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1632 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
1733 </head>
1834 <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>
1942 {{if .User}}
2043 <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">
2144 <div class="mb-8 px-3">
@@ -43,17 +66,15 @@
4366 Library
4467 </a>
4568 </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>
5273 </a>
53- <form method="POST" action="/auth/logout">
74+ <form method="POST" action="/auth/logout" class="shrink-0">
5475 {{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>
5778 </button>
5879 </form>
5980 </div>
@@ -113,13 +134,13 @@
113134 <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>
114135 Trending
115136 </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>
119137 <a href="/articles" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition">
120138 <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>
121139 Articles
122140 </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>
123144 <a href="/feeds" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition">
124145 <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>
125146 Feeds
@@ -140,7 +161,7 @@
140161 </div>
141162 </div>
142163 <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>
144165 <div class="text-[10px] text-spot-muted mt-0.5">Built on the AT Protocol</div>
145166 </div>
146167 </div>
@@ -199,6 +220,15 @@
199220 }
200221 updateThemeIcons(document.documentElement.getAttribute('data-theme') || 'dark');
201222
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+ }
231+
202232 document.addEventListener('keydown', function(e) {
203233 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
204234 if (e.key === 'g') window.location.href = '/dashboard';
@@ -10,12 +10,35 @@
10 </script>10 </script>
11 <link rel="stylesheet" href="/static/output.css">11 <link rel="stylesheet" href="/static/output.css">
12 <script src="https://unpkg.com/htmx.org@2"></script>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 <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">29 <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
14 <link rel="preconnect" href="https://fonts.googleapis.com">30 <link rel="preconnect" href="https://fonts.googleapis.com">
15 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>31 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">32 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
17 </head>33 </head>
18 <body class="bg-spot-bg text-spot-text min-h-screen flex">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 {{if .User}}42 {{if .User}}
20 <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">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 <div class="mb-8 px-3">44 <div class="mb-8 px-3">
@@ -43,17 +66,15 @@
43 Library66 Library
44 </a>67 </a>
45 </nav>68 </nav>
46- <div class="mt-auto pt-4 border-t border-spot-divider-30">69+ <div class="mt-auto pt-4 border-t border-spot-divider-30 flex items-center gap-2 px-1">
47- <a href="/profile/{{.User.DID}}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-spot-hover-50 transition">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">
48- {{if .User.AvatarURL.Valid}}<img src="{{.User.AvatarURL.String}}" class="w-8 h-8 rounded-full">{{end}}71+ {{if .User.AvatarURL.Valid}}<img src="{{.User.AvatarURL.String}}" class="w-7 h-7 rounded-full shrink-0">{{end}}
49- <div class="min-w-0 flex-1">72+ <span class="text-sm font-medium truncate text-spot-text">@{{.User.Handle}}</span>
50- <div class="text-sm font-bold truncate text-spot-text">@{{.User.Handle}}</div>
51- </div>
52 </a>73 </a>
53- <form method="POST" action="/auth/logout">74+ <form method="POST" action="/auth/logout" class="shrink-0">
54 {{csrfInput .CSRFToken}}75 {{csrfInput .CSRFToken}}
55- <button type="submit" class="text-spot-secondary hover:text-spot-red transition" title="Logout">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">
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>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 </button>78 </button>
58 </form>79 </form>
59 </div>80 </div>
@@ -113,13 +134,13 @@
113 <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>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 Trending135 Trending
115 </a>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 <a href="/articles" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition">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 <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>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 Articles139 Articles
122 </a>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 <a href="/feeds" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition">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 <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>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 Feeds146 Feeds
@@ -140,7 +161,7 @@
140 </div>161 </div>
141 </div>162 </div>
142 <div class="text-right">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 <div class="text-[10px] text-spot-muted mt-0.5">Built on the AT Protocol</div>165 <div class="text-[10px] text-spot-muted mt-0.5">Built on the AT Protocol</div>
145 </div>166 </div>
146 </div>167 </div>
@@ -199,6 +220,15 @@
199 }220 }
200 updateThemeIcons(document.documentElement.getAttribute('data-theme') || 'dark');221 updateThemeIcons(document.documentElement.getAttribute('data-theme') || 'dark');
201 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+ }
231+
202 document.addEventListener('keydown', function(e) {232 document.addEventListener('keydown', function(e) {
203 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;233 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
204 if (e.key === 'g') window.location.href = '/dashboard';234 if (e.key === 'g') window.location.href = '/dashboard';
modified internal/tmpl/dashboard.html +2 -2
@@ -38,9 +38,9 @@
3838 {{end}}
3939 </div>
4040
41- {{if .HasMore}}
41+ {{if .Page.HasMore}}
4242 <div class="text-center py-4">
43- <button hx-get="/dashboard?offset={{.NextOffset}}"
43+ <button hx-get="/dashboard?offset={{.Page.NextOffset}}"
4444 hx-target="#article-list" hx-swap="beforeend"
4545 hx-select="article"
4646 class="text-sm text-spot-secondary hover:text-spot-green transition">
@@ -38,9 +38,9 @@
38 {{end}}38 {{end}}
39 </div>39 </div>
40 40
41- {{if .HasMore}}41+ {{if .Page.HasMore}}
42 <div class="text-center py-4">42 <div class="text-center py-4">
43- <button hx-get="/dashboard?offset={{.NextOffset}}"43+ <button hx-get="/dashboard?offset={{.Page.NextOffset}}"
44 hx-target="#article-list" hx-swap="beforeend"44 hx-target="#article-list" hx-swap="beforeend"
45 hx-select="article"45 hx-select="article"
46 class="text-sm text-spot-secondary hover:text-spot-green transition">46 class="text-sm text-spot-secondary hover:text-spot-green transition">
modified internal/tmpl/feeds.html +2 -2
@@ -90,9 +90,9 @@
9090 {{end}}
9191 </div>
9292
93- {{if .HasMore}}
93+ {{if .Page.HasMore}}
9494 <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}}"
9696 hx-target="#feed-list" hx-swap="beforeend"
9797 hx-select=".px-5"
9898 class="text-sm text-spot-secondary hover:text-spot-green transition">
@@ -90,9 +90,9 @@
90 {{end}}90 {{end}}
91 </div>91 </div>
92 92
93- {{if .HasMore}}93+ {{if .Page.HasMore}}
94 <div class="text-center py-4">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 hx-target="#feed-list" hx-swap="beforeend"96 hx-target="#feed-list" hx-swap="beforeend"
97 hx-select=".px-5"97 hx-select=".px-5"
98 class="text-sm text-spot-secondary hover:text-spot-green transition">98 class="text-sm text-spot-secondary hover:text-spot-green transition">
modified internal/tmpl/index.html +1 -1
@@ -139,7 +139,7 @@
139139 </div>
140140 <div class="flex items-center gap-2 text-[rgba(255,255,255,0.70)]">
141141 <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>
143143 </div>
144144 <div class="flex items-center gap-2 text-[rgba(255,255,255,0.70)]">
145145 <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>
@@ -139,7 +139,7 @@
139 </div>139 </div>
140 <div class="flex items-center gap-2 text-[rgba(255,255,255,0.70)]">140 <div class="flex items-center gap-2 text-[rgba(255,255,255,0.70)]">
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>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 source142+ <a href="https://tangled.org/julien.rbrt.fr/glean" class="hover:text-white transition">Open source</a>
143 </div>143 </div>
144 <div class="flex items-center gap-2 text-[rgba(255,255,255,0.70)]">144 <div class="flex items-center gap-2 text-[rgba(255,255,255,0.70)]">
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>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>
modified internal/tmpl/partials/article-card.html +17 -1
@@ -1,5 +1,5 @@
11 {{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">
33 <div class="flex items-start justify-between gap-4">
44 <div class="min-w-0 flex-1">
55 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>
@@ -14,7 +14,23 @@
1414 </div>
1515 <div class="flex flex-col items-center gap-1 shrink-0">
1616 {{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>
1720 </div>
1821 </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>
34+ </div>
1935 </article>
2036 {{end}}
@@ -1,5 +1,5 @@
1 {{define "article-card.html"}}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 <div class="flex items-start justify-between gap-4">3 <div class="flex items-start justify-between gap-4">
4 <div class="min-w-0 flex-1">4 <div class="min-w-0 flex-1">
5 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>5 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>
@@ -14,7 +14,23 @@
14 </div>14 </div>
15 <div class="flex flex-col items-center gap-1 shrink-0">15 <div class="flex flex-col items-center gap-1 shrink-0">
16 {{template "like-button.html" .}}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 </div>20 </div>
18 </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>
34+ </div>
19 </article>35 </article>
20 {{end}}36 {{end}}
modified internal/tmpl/partials/logo.html +13 -8
@@ -1,16 +1,21 @@
11 {{define "logo-icon"}}<svg class="w-full h-full" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
22 <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"/>
914 </svg>{{end}}
1015
1116 {{define "logo-link"}}<a href="/" class="text-spot-green font-bold text-xl tracking-tight flex items-center gap-2">
1217 <span class="w-7 h-7">{{template "logo-icon"}}</span>
13- Glean
18+ Glean.at
1419 </a>{{end}}
1520
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}}
@@ -1,16 +1,21 @@
1 {{define "logo-icon"}}<svg class="w-full h-full" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">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 <rect width="32" height="32" rx="8" fill="#00754A"/>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"/>3+ <ellipse cx="16" cy="20" rx="5.5" ry="6" fill="#fff"/>
4- <path d="M16 11 Q11 9 9 13" stroke="#fff" stroke-width="1.8" stroke-linecap="round" fill="none"/>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="M16 11 Q21 9 23 13" stroke="#fff" stroke-width="1.8" stroke-linecap="round" fill="none"/>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- <path d="M16 15 Q10 13 8 18" stroke="#fff" stroke-width="1.8" stroke-linecap="round" fill="none"/>6+ <line x1="13" y1="17" x2="19" y2="17" stroke="#00754A" stroke-width="1.2"/>
7- <path d="M16 15 Q22 13 24 18" stroke="#fff" stroke-width="1.8" stroke-linecap="round" fill="none"/>7+ <line x1="12.5" y1="20" x2="19.5" y2="20" stroke="#00754A" stroke-width="1.2"/>
8- <circle cx="16" cy="24" r="1.5" fill="#fff"/>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 </svg>{{end}}14 </svg>{{end}}
10 15
11 {{define "logo-link"}}<a href="/" class="text-spot-green font-bold text-xl tracking-tight flex items-center gap-2">16 {{define "logo-link"}}<a href="/" class="text-spot-green font-bold text-xl tracking-tight flex items-center gap-2">
12 <span class="w-7 h-7">{{template "logo-icon"}}</span>17 <span class="w-7 h-7">{{template "logo-icon"}}</span>
13- Glean18+ Glean.at
14 </a>{{end}}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}}
modified readme.md +1 -1
@@ -1 +1 @@
1-# Glean
1+# [Glean.at](https://glean.at)
@@ -1 +1 @@
1-# Glean1+# [Glean.at](https://glean.at)
modified static/favicon.svg +11 -8
@@ -1,11 +1,14 @@
11 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
22 <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"/>
1114 </svg>
@@ -1,11 +1,14 @@
1 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">1 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
2 <rect width="32" height="32" rx="8" fill="#1E3932"/>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"/>3+ <ellipse cx="16" cy="20" rx="5.5" ry="6" fill="#00754A"/>
4- <path d="M16 10 Q10 8 8 12" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/>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="M16 10 Q22 8 24 12" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/>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- <path d="M16 14 Q9 12 7 17" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/>6+ <line x1="13" y1="17" x2="19" y2="17" stroke="#1E3932" stroke-width="1.2"/>
7- <path d="M16 14 Q23 12 25 17" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/>7+ <line x1="12.5" y1="20" x2="19.5" y2="20" stroke="#1E3932" stroke-width="1.2"/>
8- <path d="M16 18 Q11 16 9 21" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/>8+ <line x1="13" y1="23" x2="19" y2="23" stroke="#1E3932" stroke-width="1.2"/>
9- <path d="M16 18 Q21 16 23 21" stroke="#00754A" stroke-width="2" stroke-linecap="round" fill="none"/>9+ <circle cx="13.5" cy="11" r="1.8" fill="#00754A"/>
10- <circle cx="16" cy="25" r="2" 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 </svg>14 </svg>