Improve logo and refactor pagination, add confirm dialogUnverified
10cff72 parent: e9f75c1 modified
internal/server/annotations_handler.go +37 -0 | @@ -4,12 +4,49 @@ import ( | ||
| 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 | ) |
| 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 | 50 | func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) { |
| 14 | 51 | user := currentUser(r) |
| 15 | 52 | 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) { | ||
| 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 | |
| @@ -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 | return | 45 | return |
| 51 | } | 46 | } |
| 52 | 47 | ||
| 53 | - hasMore := len(articles) > pageLimit | 48 | + 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 | ||
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "net/http" |
| 5 | - "strconv" | |
| 6 | 5 | "time" |
| 7 | 6 | ) |
| 8 | 7 | |
| @@ -12,16 +11,11 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | ||
| 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,15 +26,14 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | ||
| 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 | } |
| @@ -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 = 0 | 16 | + page = page.Paginate(len(articles)) |
| 18 | - } | 17 | + if page.HasMore { |
| 19 | - pageLimit := 25 | 18 | + 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) { | ||
| 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,8 +47,7 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) { | ||
| 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 | |
| @@ -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 = 0 | 23 | + page = page.Paginate(len(subs)) |
| 24 | - } | 24 | + if page.HasMore { |
| 25 | - pageLimit := 50 | 25 | + 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/server/trending_handler.go +10 -17 | @@ -2,7 +2,6 @@ package server | ||
| 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,31 +15,25 @@ func (s *Server) handleTrending(w http.ResponseWriter, r *http.Request) { | ||
| 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,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 | "pkg.rbrt.fr/glean/internal/db" | 7 | "pkg.rbrt.fr/glean/internal/db" |
| @@ -16,31 +15,25 @@ func (s *Server) handleTrending(w http.ResponseWriter, r *http.Request) { | |||
| 16 | scope = "for-me" | 15 | scope = "for-me" |
| 17 | } | 16 | } |
| 18 | 17 | ||
| 19 | - offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) | 18 | + page := pageFromRequest(r, 25) |
| 20 | - if offset < 0 { | ||
| 21 | - offset = 0 | ||
| 22 | - } | ||
| 23 | - pageLimit := 25 | ||
| 24 | - | ||
| 25 | since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339) | 19 | since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339) |
| 26 | 20 | ||
| 27 | var trending []*db.TrendingItem | 21 | var trending []*db.TrendingItem |
| 28 | if scope == "for-me" { | 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 | } else { | 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 | 28 | + page = page.Paginate(len(trending)) |
| 35 | - if hasMore { | 29 | + if page.HasMore { |
| 36 | - trending = trending[:pageLimit] | 30 | + trending = trending[:page.Limit] |
| 37 | } | 31 | } |
| 38 | 32 | ||
| 39 | s.render(w, r, "trending.html", map[string]any{ | 33 | s.render(w, r, "trending.html", map[string]any{ |
| 40 | - "User": user, | 34 | + "User": user, |
| 41 | - "Trending": trending, | 35 | + "Trending": trending, |
| 42 | - "Scope": scope, | 36 | + "Scope": scope, |
| 43 | - "HasMore": hasMore, | 37 | + "Page": page, |
| 44 | - "NextOffset": offset + pageLimit, | ||
| 45 | }) | 38 | }) |
| 46 | } | 39 | } |
modified
internal/tmpl/articles.html +2 -2 | @@ -17,9 +17,9 @@ | ||
| 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"> |
| @@ -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 @@ | ||
| 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,17 +66,15 @@ | ||
| 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,13 +134,13 @@ | ||
| 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,7 +161,7 @@ | ||
| 140 | 161 | </div> |
| 141 | 162 | </div> |
| 142 | 163 | <div class="text-right"> |
| 143 | - <div class="text-xs text-spot-secondary">© {{now.Format "2006"}} Glean</div> | |
| 164 | + <div class="text-xs text-spot-secondary">© {{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> |
| @@ -199,6 +220,15 @@ | ||
| 199 | 220 | } |
| 200 | 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 | 232 | document.addEventListener('keydown', function(e) { |
| 203 | 233 | if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; |
| 204 | 234 | 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 | Library | 66 | 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 | Trending | 135 | 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 | Articles | 139 | 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 | Feeds | 146 | 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">© {{now.Format "2006"}} Glean</div> | 164 | + <div class="text-xs text-spot-secondary">© {{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 @@ | ||
| 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"> |
| @@ -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 @@ | ||
| 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"> |
| @@ -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 @@ | ||
| 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> |
| @@ -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 source | 142 | + <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 @@ | ||
| 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,7 +14,23 @@ | ||
| 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> |
| 18 | 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 | 35 | </article> |
| 20 | 36 | {{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 @@ | ||
| 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}} | |
| @@ -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 | - Glean | 18 | + 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
internal/tmpl/trending.html +2 -2 | @@ -37,9 +37,9 @@ | ||
| 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"> |
| @@ -37,9 +37,9 @@ | |||
| 37 | {{end}} | 37 | {{end}} |
| 38 | </div> | 38 | </div> |
| 39 | 39 | ||
| 40 | -{{if .HasMore}} | 40 | +{{if .Page.HasMore}} |
| 41 | <div class="text-center py-4"> | 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 | hx-target="#trending-list" hx-swap="beforeend" | 43 | hx-target="#trending-list" hx-swap="beforeend" |
| 44 | hx-select="article" | 44 | hx-select="article" |
| 45 | class="text-sm text-spot-secondary hover:text-spot-green transition"> | 45 | class="text-sm text-spot-secondary hover:text-spot-green transition"> |
modified
readme.md +1 -1 | @@ -1 +1 @@ | ||
| 1 | -# Glean | |
| 1 | +# [Glean.at](https://glean.at) | |
| @@ -1 +1 @@ | |||
| 1 | -# Glean | 1 | +# [Glean.at](https://glean.at) |
modified
static/favicon.svg +11 -8 | @@ -1,11 +1,14 @@ | ||
| 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> |
| @@ -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> |