nandi/gleanpublic⑂ Fork 0
⑂ e9f75c1
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.

Implement pagination and social trending featuresUnverified

Julien Robert committed 2026-04-20T16:04:22+02:00 Browse files
e9f75c1 parent: 2c8faf0
modified internal/db/social.go +69 -0
@@ -190,6 +190,45 @@ type TrendingItem struct {
190190 AnnotationCount int
191191 }
192192
193+func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) {
194+ rows, err := db.QueryContext(ctx, fmt.Sprintf(`
195+ SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
196+ COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),
197+ COUNT(DISTINCT l.id) AS like_count,
198+ COUNT(DISTINCT a.id) AS annotation_count
199+ FROM likes l
200+ JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url
201+ LEFT JOIN feeds f ON f.feed_url = l.feed_url
202+ LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ?
203+ WHERE l.created_at >= ?
204+ AND l.author_did IN (
205+ SELECT CASE WHEN us.user_a = ? THEN us.user_b ELSE us.user_a END
206+ FROM user_similarity us
207+ WHERE us.user_a = ? OR us.user_b = ?
208+ UNION SELECT ?
209+ )
210+ GROUP BY ar.id
211+ ORDER BY like_count DESC, annotation_count DESC
212+ LIMIT %d OFFSET %d
213+ `, limit, offset), since, since, userDID, userDID, userDID, userDID)
214+ if err != nil {
215+ return nil, err
216+ }
217+ defer rows.Close()
218+
219+ var results []*TrendingItem
220+ for rows.Next() {
221+ item := &TrendingItem{}
222+ if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,
223+ &item.Summary, &item.FeedURL, &item.FeedTitle,
224+ &item.LikeCount, &item.AnnotationCount); err != nil {
225+ return nil, err
226+ }
227+ results = append(results, item)
228+ }
229+ return results, rows.Err()
230+}
231+
193232 func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, offset int) ([]*TrendingItem, error) {
194233 rows, err := db.QueryContext(ctx, fmt.Sprintf(`
195234 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
@@ -222,3 +261,33 @@ func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, off
222261 }
223262 return results, rows.Err()
224263 }
264+
265+func (db *DB) ListLikedArticles(ctx context.Context, userDID string, limit, offset int) ([]*Article, error) {
266+ rows, err := db.QueryContext(ctx, fmt.Sprintf(`
267+ SELECT DISTINCT a.id, a.feed_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
268+ a.published, a.updated, a.fetched_at,
269+ COALESCE(f.title, '')
270+ FROM likes l
271+ JOIN articles a ON a.url = l.article_url AND a.feed_url = l.feed_url
272+ LEFT JOIN feeds f ON f.feed_url = a.feed_url
273+ WHERE l.author_did = ?
274+ ORDER BY l.created_at DESC
275+ LIMIT %d OFFSET %d
276+ `, limit, offset), userDID)
277+ if err != nil {
278+ return nil, err
279+ }
280+ defer rows.Close()
281+
282+ var articles []*Article
283+ for rows.Next() {
284+ a := &Article{}
285+ if err := rows.Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author,
286+ &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
287+ &a.FeedTitle); err != nil {
288+ return nil, err
289+ }
290+ articles = append(articles, a)
291+ }
292+ return articles, rows.Err()
293+}
@@ -190,6 +190,45 @@ type TrendingItem struct {
190 AnnotationCount int190 AnnotationCount int
191 }191 }
192 192
193+func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) {
194+ rows, err := db.QueryContext(ctx, fmt.Sprintf(`
195+ SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
196+ COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),
197+ COUNT(DISTINCT l.id) AS like_count,
198+ COUNT(DISTINCT a.id) AS annotation_count
199+ FROM likes l
200+ JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url
201+ LEFT JOIN feeds f ON f.feed_url = l.feed_url
202+ LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ?
203+ WHERE l.created_at >= ?
204+ AND l.author_did IN (
205+ SELECT CASE WHEN us.user_a = ? THEN us.user_b ELSE us.user_a END
206+ FROM user_similarity us
207+ WHERE us.user_a = ? OR us.user_b = ?
208+ UNION SELECT ?
209+ )
210+ GROUP BY ar.id
211+ ORDER BY like_count DESC, annotation_count DESC
212+ LIMIT %d OFFSET %d
213+ `, limit, offset), since, since, userDID, userDID, userDID, userDID)
214+ if err != nil {
215+ return nil, err
216+ }
217+ defer rows.Close()
218+
219+ var results []*TrendingItem
220+ for rows.Next() {
221+ item := &TrendingItem{}
222+ if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,
223+ &item.Summary, &item.FeedURL, &item.FeedTitle,
224+ &item.LikeCount, &item.AnnotationCount); err != nil {
225+ return nil, err
226+ }
227+ results = append(results, item)
228+ }
229+ return results, rows.Err()
230+}
231+
193 func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, offset int) ([]*TrendingItem, error) {232 func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, offset int) ([]*TrendingItem, error) {
194 rows, err := db.QueryContext(ctx, fmt.Sprintf(`233 rows, err := db.QueryContext(ctx, fmt.Sprintf(`
195 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),234 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
@@ -222,3 +261,33 @@ func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, off
222 }261 }
223 return results, rows.Err()262 return results, rows.Err()
224 }263 }
264+
265+func (db *DB) ListLikedArticles(ctx context.Context, userDID string, limit, offset int) ([]*Article, error) {
266+ rows, err := db.QueryContext(ctx, fmt.Sprintf(`
267+ SELECT DISTINCT a.id, a.feed_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
268+ a.published, a.updated, a.fetched_at,
269+ COALESCE(f.title, '')
270+ FROM likes l
271+ JOIN articles a ON a.url = l.article_url AND a.feed_url = l.feed_url
272+ LEFT JOIN feeds f ON f.feed_url = a.feed_url
273+ WHERE l.author_did = ?
274+ ORDER BY l.created_at DESC
275+ LIMIT %d OFFSET %d
276+ `, limit, offset), userDID)
277+ if err != nil {
278+ return nil, err
279+ }
280+ defer rows.Close()
281+
282+ var articles []*Article
283+ for rows.Next() {
284+ a := &Article{}
285+ if err := rows.Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author,
286+ &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
287+ &a.FeedTitle); err != nil {
288+ return nil, err
289+ }
290+ articles = append(articles, a)
291+ }
292+ return articles, rows.Err()
293+}
modified internal/server/annotations_handler.go +0 -11
@@ -10,17 +10,6 @@ import (
1010 "pkg.rbrt.fr/glean/internal/db"
1111 )
1212
13-func (s *Server) handleAnnotations(w http.ResponseWriter, r *http.Request) {
14- user := currentUser(r)
15- articleURL := r.URL.Query().Get("article")
16- annotations, _ := s.db.ListAnnotations(r.Context(), "", articleURL, "", 50, 0)
17- s.render(w, r, "annotations.html", map[string]any{
18- "User": user,
19- "Annotations": annotations,
20- "ArticleURL": articleURL,
21- })
22-}
23-
2413 func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) {
2514 user := currentUser(r)
2615 a := &db.Annotation{
@@ -10,17 +10,6 @@ import (
10 "pkg.rbrt.fr/glean/internal/db"10 "pkg.rbrt.fr/glean/internal/db"
11 )11 )
12 12
13-func (s *Server) handleAnnotations(w http.ResponseWriter, r *http.Request) {
14- user := currentUser(r)
15- articleURL := r.URL.Query().Get("article")
16- annotations, _ := s.db.ListAnnotations(r.Context(), "", articleURL, "", 50, 0)
17- s.render(w, r, "annotations.html", map[string]any{
18- "User": user,
19- "Annotations": annotations,
20- "ArticleURL": articleURL,
21- })
22-}
23-
24 func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) {13 func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) {
25 user := currentUser(r)14 user := currentUser(r)
26 a := &db.Annotation{15 a := &db.Annotation{
modified internal/server/articles_handler.go +17 -4
@@ -37,17 +37,30 @@ func (s *Server) handleArticles(w http.ResponseWriter, r *http.Request) {
3737 user := currentUser(r)
3838 feedURL := r.URL.Query().Get("feed")
3939
40- articles, err := s.db.ListArticles(r.Context(), user.DID, feedURL, 50, 0)
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)
4147 if err != nil {
4248 s.logger.Error("failed to list articles", "error", err)
4349 http.Error(w, err.Error(), http.StatusInternalServerError)
4450 return
4551 }
4652
53+ hasMore := len(articles) > pageLimit
54+ if hasMore {
55+ articles = articles[:pageLimit]
56+ }
57+
4758 s.render(w, r, "articles.html", map[string]any{
48- "User": user,
49- "Articles": articles,
50- "FeedURL": feedURL,
59+ "User": user,
60+ "Articles": articles,
61+ "FeedURL": feedURL,
62+ "HasMore": hasMore,
63+ "NextOffset": offset + pageLimit,
5164 })
5265 }
5366
@@ -37,17 +37,30 @@ 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- articles, err := s.db.ListArticles(r.Context(), user.DID, feedURL, 50, 0)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)
41 if err != nil {47 if err != nil {
42 s.logger.Error("failed to list articles", "error", err)48 s.logger.Error("failed to list articles", "error", err)
43 http.Error(w, err.Error(), http.StatusInternalServerError)49 http.Error(w, err.Error(), http.StatusInternalServerError)
44 return50 return
45 }51 }
46 52
53+ hasMore := len(articles) > pageLimit
54+ if hasMore {
55+ articles = articles[:pageLimit]
56+ }
57+
47 s.render(w, r, "articles.html", map[string]any{58 s.render(w, r, "articles.html", map[string]any{
48- "User": user,59+ "User": user,
49- "Articles": articles,60+ "Articles": articles,
50- "FeedURL": feedURL,61+ "FeedURL": feedURL,
62+ "HasMore": hasMore,
63+ "NextOffset": offset + pageLimit,
51 })64 })
52 }65 }
53 66
modified internal/server/dashboard_handler.go +22 -4
@@ -2,6 +2,7 @@ package server
22
33 import (
44 "net/http"
5+ "strconv"
56 "time"
67 )
78
@@ -10,19 +11,36 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
1011
1112 unreadCount, _ := s.db.GetUnreadCount(r.Context(), user.DID, "")
1213 subCount, _ := s.db.GetSubscriptionCount(r.Context(), user.DID)
13- articles, _ := s.db.ListUnreadArticles(r.Context(), user.DID, "", 25, 0)
14- feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 5)
14+
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]
25+ }
26+
27+ articleRecs, _ := s.db.GetArticleRecommendations(r.Context(), user.DID, 5)
1528 peopleRecs, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 5)
29+ feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 5)
30+
1631 since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)
17- trending, _ := s.db.ListTrendingArticles(r.Context(), since, 5, 0)
32+ personalTrending, _ := s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, 5, 0)
1833
1934 s.render(w, r, "dashboard.html", map[string]any{
2035 "User": user,
2136 "UnreadCount": unreadCount,
2237 "SubscriptionCount": subCount,
2338 "Articles": articles,
39+ "ArticleRecommendations": articleRecs,
2440 "FeedRecommendations": feedRecs,
2541 "PeopleRecommendations": peopleRecs,
26- "Trending": trending,
42+ "PersonalTrending": personalTrending,
43+ "HasMore": hasMore,
44+ "NextOffset": offset + pageLimit,
2745 })
2846 }
@@ -2,6 +2,7 @@ package server
2 2
3 import (3 import (
4 "net/http"4 "net/http"
5+ "strconv"
5 "time"6 "time"
6 )7 )
7 8
@@ -10,19 +11,36 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
10 11
11 unreadCount, _ := s.db.GetUnreadCount(r.Context(), user.DID, "")12 unreadCount, _ := s.db.GetUnreadCount(r.Context(), user.DID, "")
12 subCount, _ := s.db.GetSubscriptionCount(r.Context(), user.DID)13 subCount, _ := s.db.GetSubscriptionCount(r.Context(), user.DID)
13- articles, _ := s.db.ListUnreadArticles(r.Context(), user.DID, "", 25, 0)14+
14- feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 5)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]
25+ }
26+
27+ articleRecs, _ := s.db.GetArticleRecommendations(r.Context(), user.DID, 5)
15 peopleRecs, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 5)28 peopleRecs, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 5)
29+ feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 5)
30+
16 since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)31 since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)
17- trending, _ := s.db.ListTrendingArticles(r.Context(), since, 5, 0)32+ personalTrending, _ := s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, 5, 0)
18 33
19 s.render(w, r, "dashboard.html", map[string]any{34 s.render(w, r, "dashboard.html", map[string]any{
20 "User": user,35 "User": user,
21 "UnreadCount": unreadCount,36 "UnreadCount": unreadCount,
22 "SubscriptionCount": subCount,37 "SubscriptionCount": subCount,
23 "Articles": articles,38 "Articles": articles,
39+ "ArticleRecommendations": articleRecs,
24 "FeedRecommendations": feedRecs,40 "FeedRecommendations": feedRecs,
25 "PeopleRecommendations": peopleRecs,41 "PeopleRecommendations": peopleRecs,
26- "Trending": trending,42+ "PersonalTrending": personalTrending,
43+ "HasMore": hasMore,
44+ "NextOffset": offset + pageLimit,
27 })45 })
28 }46 }
deleted internal/server/discover_handler.go +0 -36
deleted file mode 100644
@@ -1,36 +0,0 @@
1-package server
2-
3-import "net/http"
4-
5-func (s *Server) handleDiscover(w http.ResponseWriter, r *http.Request) {
6- user := currentUser(r)
7- feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 20)
8- people, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 20)
9- popular, _ := s.db.ListUnsubscribedFeeds(r.Context(), user.DID, 20, 0)
10- articleRecs, _ := s.db.GetArticleRecommendations(r.Context(), user.DID, 20)
11- s.render(w, r, "discover.html", map[string]any{
12- "User": user,
13- "FeedRecommendations": feedRecs,
14- "PeopleRecommendations": people,
15- "PopularFeeds": popular,
16- "ArticleRecommendations": articleRecs,
17- })
18-}
19-
20-func (s *Server) handleDiscoverFeeds(w http.ResponseWriter, r *http.Request) {
21- user := currentUser(r)
22- feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 20)
23- s.render(w, r, "discover.html", map[string]any{
24- "User": user,
25- "FeedRecommendations": feedRecs,
26- })
27-}
28-
29-func (s *Server) handleDiscoverPeople(w http.ResponseWriter, r *http.Request) {
30- user := currentUser(r)
31- people, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 20)
32- s.render(w, r, "discover.html", map[string]any{
33- "User": user,
34- "PeopleRecommendations": people,
35- })
36-}
deleted file mode 100644
@@ -1,36 +0,0 @@
1-package server
2-
3-import "net/http"
4-
5-func (s *Server) handleDiscover(w http.ResponseWriter, r *http.Request) {
6- user := currentUser(r)
7- feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 20)
8- people, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 20)
9- popular, _ := s.db.ListUnsubscribedFeeds(r.Context(), user.DID, 20, 0)
10- articleRecs, _ := s.db.GetArticleRecommendations(r.Context(), user.DID, 20)
11- s.render(w, r, "discover.html", map[string]any{
12- "User": user,
13- "FeedRecommendations": feedRecs,
14- "PeopleRecommendations": people,
15- "PopularFeeds": popular,
16- "ArticleRecommendations": articleRecs,
17- })
18-}
19-
20-func (s *Server) handleDiscoverFeeds(w http.ResponseWriter, r *http.Request) {
21- user := currentUser(r)
22- feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 20)
23- s.render(w, r, "discover.html", map[string]any{
24- "User": user,
25- "FeedRecommendations": feedRecs,
26- })
27-}
28-
29-func (s *Server) handleDiscoverPeople(w http.ResponseWriter, r *http.Request) {
30- user := currentUser(r)
31- people, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 20)
32- s.render(w, r, "discover.html", map[string]any{
33- "User": user,
34- "PeopleRecommendations": people,
35- })
36-}
modified internal/server/feeds_handler.go +16 -2
@@ -17,8 +17,20 @@ import (
1717 func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
1818 user := currentUser(r)
1919 category := r.URL.Query().Get("category")
20- subs, _ := s.db.ListSubscriptions(r.Context(), user.DID, category, 100, 0)
21- allSubs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 100, 0)
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]
31+ }
32+
33+ allSubs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 1000, 0)
2234 feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 10)
2335 peopleRecs, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 5)
2436 deadFeeds, _ := s.db.ListDeadFeeds(r.Context(), user.DID, 7)
@@ -40,6 +52,8 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
4052 "FeedRecommendations": feedRecs,
4153 "PeopleRecommendations": peopleRecs,
4254 "DeadFeeds": deadFeeds,
55+ "HasMore": hasMore,
56+ "NextOffset": offset + pageLimit,
4357 })
4458 }
4559
@@ -17,8 +17,20 @@ import (
17 func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {17 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- subs, _ := s.db.ListSubscriptions(r.Context(), user.DID, category, 100, 0)20+
21- allSubs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 100, 0)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]
31+ }
32+
33+ allSubs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 1000, 0)
22 feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 10)34 feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 10)
23 peopleRecs, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 5)35 peopleRecs, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 5)
24 deadFeeds, _ := s.db.ListDeadFeeds(r.Context(), user.DID, 7)36 deadFeeds, _ := s.db.ListDeadFeeds(r.Context(), user.DID, 7)
@@ -40,6 +52,8 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
40 "FeedRecommendations": feedRecs,52 "FeedRecommendations": feedRecs,
41 "PeopleRecommendations": peopleRecs,53 "PeopleRecommendations": peopleRecs,
42 "DeadFeeds": deadFeeds,54 "DeadFeeds": deadFeeds,
55+ "HasMore": hasMore,
56+ "NextOffset": offset + pageLimit,
43 })57 })
44 }58 }
45 59
added internal/server/library_handler.go +44 -0
new file mode 100644
@@ -0,0 +1,44 @@
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+}
new file mode 100644
@@ -0,0 +1,44 @@
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+}
modified internal/server/server.go +2 -9
@@ -124,21 +124,14 @@ func (s *Server) setupRoutes() {
124124 r.Get("/", s.handleTrending)
125125 })
126126
127- s.router.Route("/discover", func(r chi.Router) {
128- r.Use(s.requireAuth)
129- r.Get("/", s.handleDiscover)
130- r.Get("/feeds", s.handleDiscoverFeeds)
131- r.Get("/people", s.handleDiscoverPeople)
132- })
133-
134127 s.router.Route("/profile", func(r chi.Router) {
135128 r.Use(s.requireAuth)
136129 r.Get("/{did}", s.handleProfile)
137130 })
138131
139- s.router.Route("/annotations", func(r chi.Router) {
132+ s.router.Route("/library", func(r chi.Router) {
140133 r.Use(s.requireAuth)
141- r.Get("/", s.handleAnnotations)
134+ r.Get("/", s.handleLibrary)
142135 r.Post("/create", s.handleCreateAnnotation)
143136 })
144137
@@ -124,21 +124,14 @@ func (s *Server) setupRoutes() {
124 r.Get("/", s.handleTrending)124 r.Get("/", s.handleTrending)
125 })125 })
126 126
127- s.router.Route("/discover", func(r chi.Router) {
128- r.Use(s.requireAuth)
129- r.Get("/", s.handleDiscover)
130- r.Get("/feeds", s.handleDiscoverFeeds)
131- r.Get("/people", s.handleDiscoverPeople)
132- })
133-
134 s.router.Route("/profile", func(r chi.Router) {127 s.router.Route("/profile", func(r chi.Router) {
135 r.Use(s.requireAuth)128 r.Use(s.requireAuth)
136 r.Get("/{did}", s.handleProfile)129 r.Get("/{did}", s.handleProfile)
137 })130 })
138 131
139- s.router.Route("/annotations", func(r chi.Router) {132+ s.router.Route("/library", func(r chi.Router) {
140 r.Use(s.requireAuth)133 r.Use(s.requireAuth)
141- r.Get("/", s.handleAnnotations)134+ r.Get("/", s.handleLibrary)
142 r.Post("/create", s.handleCreateAnnotation)135 r.Post("/create", s.handleCreateAnnotation)
143 })136 })
144 137
deleted internal/tmpl/annotations.html +0 -34
deleted file mode 100644
@@ -1,34 +0,0 @@
1-{{define "annotations.html"}}
2-<div class="flex items-center justify-between mb-2">
3- <h1 class="text-2xl font-bold text-spot-text">Annotations</h1>
4- <div></div>
5-</div>
6-<p class="text-sm text-spot-secondary mb-6">Notes and highlights from across your reading.</p>
7-
8-{{if .ArticleURL}}
9-<div class="mb-4 text-sm text-spot-secondary">
10- Filtering by: <strong class="text-spot-text">{{.ArticleURL}}</strong>
11- <a href="/annotations" class="text-spot-green ml-2 hover:brightness-110">Clear</a>
12-</div>
13-{{end}}
14-
15-<div class="space-y-4" id="annotations-list">
16- {{range .Annotations}}
17- {{template "annotation-card.html" .}}
18- {{else}}
19- <div class="text-center text-spot-secondary py-12">
20- <p>No annotations yet.</p>
21- <p class="text-sm mt-1">Highlight and annotate articles as you read.</p>
22- </div>
23- {{end}}
24-</div>
25-
26-{{if .HasMore}}
27-<div class="text-center py-4">
28- <button hx-get="/annotations?offset={{.NextOffset}}{{if .ArticleURL}}&article={{.ArticleURL}}{{end}}"
29- hx-target="#annotations-list" hx-swap="beforeend"
30- hx-select=".annotation-card"
31- class="text-sm text-spot-secondary hover:text-spot-green transition">Load more</button>
32-</div>
33-{{end}}
34-{{end}}
deleted file mode 100644
@@ -1,34 +0,0 @@
1-{{define "annotations.html"}}
2-<div class="flex items-center justify-between mb-2">
3- <h1 class="text-2xl font-bold text-spot-text">Annotations</h1>
4- <div></div>
5-</div>
6-<p class="text-sm text-spot-secondary mb-6">Notes and highlights from across your reading.</p>
7-
8-{{if .ArticleURL}}
9-<div class="mb-4 text-sm text-spot-secondary">
10- Filtering by: <strong class="text-spot-text">{{.ArticleURL}}</strong>
11- <a href="/annotations" class="text-spot-green ml-2 hover:brightness-110">Clear</a>
12-</div>
13-{{end}}
14-
15-<div class="space-y-4" id="annotations-list">
16- {{range .Annotations}}
17- {{template "annotation-card.html" .}}
18- {{else}}
19- <div class="text-center text-spot-secondary py-12">
20- <p>No annotations yet.</p>
21- <p class="text-sm mt-1">Highlight and annotate articles as you read.</p>
22- </div>
23- {{end}}
24-</div>
25-
26-{{if .HasMore}}
27-<div class="text-center py-4">
28- <button hx-get="/annotations?offset={{.NextOffset}}{{if .ArticleURL}}&article={{.ArticleURL}}{{end}}"
29- hx-target="#annotations-list" hx-swap="beforeend"
30- hx-select=".annotation-card"
31- class="text-sm text-spot-secondary hover:text-spot-green transition">Load more</button>
32-</div>
33-{{end}}
34-{{end}}
modified internal/tmpl/article_detail.html +1 -1
@@ -66,7 +66,7 @@
6666 <section>
6767 <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2>
6868
69- <form hx-post="/annotations/create" hx-target="#annotations-list" hx-swap="beforeend"
69+ <form hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"
7070 hx-on::after-request="this.reset()"
7171 class="bg-spot-surface rounded-xl shadow-spot p-4 mb-4 space-y-3">
7272 {{csrfInput .CSRFToken}}
@@ -66,7 +66,7 @@
66 <section>66 <section>
67 <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2>67 <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2>
68 68
69- <form hx-post="/annotations/create" hx-target="#annotations-list" hx-swap="beforeend"69+ <form hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"
70 hx-on::after-request="this.reset()"70 hx-on::after-request="this.reset()"
71 class="bg-spot-surface rounded-xl shadow-spot p-4 mb-4 space-y-3">71 class="bg-spot-surface rounded-xl shadow-spot p-4 mb-4 space-y-3">
72 {{csrfInput .CSRFToken}}72 {{csrfInput .CSRFToken}}
modified internal/tmpl/base.html +6 -19
@@ -38,13 +38,9 @@
3838 <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="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
3939 Trending
4040 </a>
41- <a href="/discover" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded-md {{activeClass .ActivePath "/discover"}}">
42- <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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
43- Discover
44- </a>
45- <a href="/annotations" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded-md {{activeClass .ActivePath "/annotations"}}">
41+ <a href="/library" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded-md {{activeClass .ActivePath "/library"}}">
4642 <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="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>
47- Annotations
43+ Library
4844 </a>
4945 </nav>
5046 <div class="mt-auto pt-4 border-t border-spot-divider-30">
@@ -81,10 +77,6 @@
8177 <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="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
8278 Trending
8379 </a>
84- <a href="/discover" class="flex flex-col items-center gap-0.5 {{activeClass .ActivePath "/discover"}} text-[10px] px-2 py-1">
85- <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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
86- Discover
87- </a>
8880 </div>
8981 </nav>
9082 {{end}}
@@ -121,10 +113,6 @@
121113 <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>
122114 Trending
123115 </a>
124- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition">
125- <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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
126- Discover
127- </a>
128116 </div>
129117 <div class="flex flex-col gap-1.5">
130118 <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Library</div>
@@ -136,9 +124,9 @@
136124 <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>
137125 Feeds
138126 </a>
139- <a href="/annotations" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition">
127+ <a href="/library" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition">
140128 <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="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>
141- Annotations
129+ Library
142130 </a>
143131 </div>
144132 <div class="flex flex-col gap-1.5">
@@ -164,13 +152,12 @@
164152 <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Browse</div>
165153 <a href="/dashboard" class="text-xs text-spot-secondary hover:text-spot-text transition">Dashboard</a>
166154 <a href="/trending" class="text-xs text-spot-secondary hover:text-spot-text transition">Trending</a>
167- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-text transition">Discover</a>
168155 </div>
169156 <div class="flex flex-col gap-1.5">
170157 <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Library</div>
171158 <a href="/articles" class="text-xs text-spot-secondary hover:text-spot-text transition">Articles</a>
172159 <a href="/feeds" class="text-xs text-spot-secondary hover:text-spot-text transition">Feeds</a>
173- <a href="/annotations" class="text-xs text-spot-secondary hover:text-spot-text transition">Annotations</a>
160+ <a href="/library" class="text-xs text-spot-secondary hover:text-spot-text transition">Library</a>
174161 </div>
175162 </div>
176163 <div class="flex items-center justify-between">
@@ -218,7 +205,7 @@
218205 if (e.key === 'a') window.location.href = '/articles';
219206 if (e.key === 'f') window.location.href = '/feeds';
220207 if (e.key === 't') window.location.href = '/trending';
221- if (e.key === 'd') window.location.href = '/discover';
208+ if (e.key === 'l') window.location.href = '/library';
222209 });
223210 </script>
224211 </body>
@@ -38,13 +38,9 @@
38 <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="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>38 <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="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
39 Trending39 Trending
40 </a>40 </a>
41- <a href="/discover" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded-md {{activeClass .ActivePath "/discover"}}">41+ <a href="/library" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded-md {{activeClass .ActivePath "/library"}}">
42- <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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
43- Discover
44- </a>
45- <a href="/annotations" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded-md {{activeClass .ActivePath "/annotations"}}">
46 <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="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>42 <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="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>
47- Annotations43+ Library
48 </a>44 </a>
49 </nav>45 </nav>
50 <div class="mt-auto pt-4 border-t border-spot-divider-30">46 <div class="mt-auto pt-4 border-t border-spot-divider-30">
@@ -81,10 +77,6 @@
81 <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="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>77 <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="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
82 Trending78 Trending
83 </a>79 </a>
84- <a href="/discover" class="flex flex-col items-center gap-0.5 {{activeClass .ActivePath "/discover"}} text-[10px] px-2 py-1">
85- <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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
86- Discover
87- </a>
88 </div>80 </div>
89 </nav>81 </nav>
90 {{end}}82 {{end}}
@@ -121,10 +113,6 @@
121 <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>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>
122 Trending114 Trending
123 </a>115 </a>
124- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition">
125- <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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
126- Discover
127- </a>
128 </div>116 </div>
129 <div class="flex flex-col gap-1.5">117 <div class="flex flex-col gap-1.5">
130 <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Library</div>118 <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Library</div>
@@ -136,9 +124,9 @@
136 <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>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>
137 Feeds125 Feeds
138 </a>126 </a>
139- <a href="/annotations" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition">127+ <a href="/library" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition">
140 <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="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>128 <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="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>
141- Annotations129+ Library
142 </a>130 </a>
143 </div>131 </div>
144 <div class="flex flex-col gap-1.5">132 <div class="flex flex-col gap-1.5">
@@ -164,13 +152,12 @@
164 <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Browse</div>152 <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Browse</div>
165 <a href="/dashboard" class="text-xs text-spot-secondary hover:text-spot-text transition">Dashboard</a>153 <a href="/dashboard" class="text-xs text-spot-secondary hover:text-spot-text transition">Dashboard</a>
166 <a href="/trending" class="text-xs text-spot-secondary hover:text-spot-text transition">Trending</a>154 <a href="/trending" class="text-xs text-spot-secondary hover:text-spot-text transition">Trending</a>
167- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-text transition">Discover</a>
168 </div>155 </div>
169 <div class="flex flex-col gap-1.5">156 <div class="flex flex-col gap-1.5">
170 <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Library</div>157 <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Library</div>
171 <a href="/articles" class="text-xs text-spot-secondary hover:text-spot-text transition">Articles</a>158 <a href="/articles" class="text-xs text-spot-secondary hover:text-spot-text transition">Articles</a>
172 <a href="/feeds" class="text-xs text-spot-secondary hover:text-spot-text transition">Feeds</a>159 <a href="/feeds" class="text-xs text-spot-secondary hover:text-spot-text transition">Feeds</a>
173- <a href="/annotations" class="text-xs text-spot-secondary hover:text-spot-text transition">Annotations</a>160+ <a href="/library" class="text-xs text-spot-secondary hover:text-spot-text transition">Library</a>
174 </div>161 </div>
175 </div>162 </div>
176 <div class="flex items-center justify-between">163 <div class="flex items-center justify-between">
@@ -218,7 +205,7 @@
218 if (e.key === 'a') window.location.href = '/articles';205 if (e.key === 'a') window.location.href = '/articles';
219 if (e.key === 'f') window.location.href = '/feeds';206 if (e.key === 'f') window.location.href = '/feeds';
220 if (e.key === 't') window.location.href = '/trending';207 if (e.key === 't') window.location.href = '/trending';
221- if (e.key === 'd') window.location.href = '/discover';208+ if (e.key === 'l') window.location.href = '/library';
222 });209 });
223 </script>210 </script>
224 </body>211 </body>
modified internal/tmpl/dashboard.html +36 -17
@@ -6,11 +6,31 @@
66 <span class="text-spot-secondary"><strong class="text-spot-text">{{.SubscriptionCount}}</strong> feeds</span>
77 </div>
88 </div>
9-<p class="text-sm text-spot-secondary mb-6">Your recent unread articles and activity.</p>
9+<p class="text-sm text-spot-secondary mb-6">Your personalized feed, based on your social graph.</p>
1010
1111 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
1212 <div class="lg:col-span-2">
13- <div class="space-y-3">
13+ {{if .ArticleRecommendations}}
14+ <div class="mb-6">
15+ <h2 class="text-lg font-semibold text-spot-text mb-3">Recommended for you</h2>
16+ <div class="space-y-3">
17+ {{range .ArticleRecommendations}}
18+ <a href="/articles/{{.ArticleID}}" class="block bg-spot-surface rounded-xl p-4 hover:bg-spot-hover-50 transition">
19+ <div class="font-bold text-sm text-spot-text leading-tight">{{.Title}}</div>
20+ <div class="flex items-center gap-2 mt-1 text-xs text-spot-secondary">
21+ {{if .Author}}<span>{{.Author}}</span>{{end}}
22+ <span class="text-spot-muted">{{.FeedTitle}}</span>
23+ {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2"}}</span>{{end}}
24+ </div>
25+ {{if .Summary}}<p class="text-xs text-spot-secondary mt-2 line-clamp-2">{{.Summary}}</p>{{end}}
26+ </a>
27+ {{end}}
28+ </div>
29+ </div>
30+ {{end}}
31+
32+ <h2 class="text-lg font-semibold text-spot-text mb-3">Unread articles</h2>
33+ <div id="article-list" class="space-y-3">
1434 {{range .Articles}}
1535 {{template "article-card.html" .}}
1636 {{else}}
@@ -18,22 +38,27 @@
1838 {{end}}
1939 </div>
2040
21- {{if .Articles}}
22- <div class="mt-4 text-center">
23- <a href="/articles" class="text-sm text-spot-secondary hover:text-spot-green transition">View all articles &rarr;</a>
41+ {{if .HasMore}}
42+ <div class="text-center py-4">
43+ <button hx-get="/dashboard?offset={{.NextOffset}}"
44+ hx-target="#article-list" hx-swap="beforeend"
45+ hx-select="article"
46+ class="text-sm text-spot-secondary hover:text-spot-green transition">
47+ Load more
48+ </button>
2449 </div>
2550 {{end}}
2651 </div>
2752
2853 <div class="space-y-8">
29- {{if .Trending}}
54+ {{if .PersonalTrending}}
3055 <div>
3156 <div class="flex items-center justify-between mb-4">
32- <h2 class="text-lg font-semibold text-spot-text">Trending</h2>
33- <a href="/trending" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>
57+ <h2 class="text-lg font-semibold text-spot-text">Trending in your network</h2>
58+ <a href="/trending?scope=for-me" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>
3459 </div>
3560 <div class="space-y-3">
36- {{range .Trending}}
61+ {{range .PersonalTrending}}
3762 <a href="/articles/{{.ArticleID}}" class="block bg-spot-surface rounded-xl p-4 hover:bg-spot-hover-50 transition">
3863 <div class="font-bold text-sm text-spot-text leading-tight">{{.Title}}</div>
3964 <div class="flex items-center gap-2 mt-1 text-xs text-spot-secondary">
@@ -52,10 +77,7 @@
5277
5378 {{if .PeopleRecommendations}}
5479 <div>
55- <div class="flex items-center justify-between mb-4">
56- <h2 class="text-lg font-semibold text-spot-text">Similar readers</h2>
57- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>
58- </div>
80+ <h2 class="text-lg font-semibold text-spot-text mb-4">Similar readers</h2>
5981 <div class="space-y-3">
6082 {{range .PeopleRecommendations}}
6183 <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">
@@ -73,10 +95,7 @@
7395
7496 {{if .FeedRecommendations}}
7597 <div>
76- <div class="flex items-center justify-between mb-4">
77- <h2 class="text-lg font-semibold text-spot-text">Recommended feeds</h2>
78- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>
79- </div>
98+ <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2>
8099 <div class="space-y-3">
81100 {{range .FeedRecommendations}}
82101 {{template "recommendation-card.html" .}}
@@ -6,11 +6,31 @@
6 <span class="text-spot-secondary"><strong class="text-spot-text">{{.SubscriptionCount}}</strong> feeds</span>6 <span class="text-spot-secondary"><strong class="text-spot-text">{{.SubscriptionCount}}</strong> feeds</span>
7 </div>7 </div>
8 </div>8 </div>
9-<p class="text-sm text-spot-secondary mb-6">Your recent unread articles and activity.</p>9+<p class="text-sm text-spot-secondary mb-6">Your personalized feed, based on your social graph.</p>
10 10
11 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">11 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
12 <div class="lg:col-span-2">12 <div class="lg:col-span-2">
13- <div class="space-y-3">13+ {{if .ArticleRecommendations}}
14+ <div class="mb-6">
15+ <h2 class="text-lg font-semibold text-spot-text mb-3">Recommended for you</h2>
16+ <div class="space-y-3">
17+ {{range .ArticleRecommendations}}
18+ <a href="/articles/{{.ArticleID}}" class="block bg-spot-surface rounded-xl p-4 hover:bg-spot-hover-50 transition">
19+ <div class="font-bold text-sm text-spot-text leading-tight">{{.Title}}</div>
20+ <div class="flex items-center gap-2 mt-1 text-xs text-spot-secondary">
21+ {{if .Author}}<span>{{.Author}}</span>{{end}}
22+ <span class="text-spot-muted">{{.FeedTitle}}</span>
23+ {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2"}}</span>{{end}}
24+ </div>
25+ {{if .Summary}}<p class="text-xs text-spot-secondary mt-2 line-clamp-2">{{.Summary}}</p>{{end}}
26+ </a>
27+ {{end}}
28+ </div>
29+ </div>
30+ {{end}}
31+
32+ <h2 class="text-lg font-semibold text-spot-text mb-3">Unread articles</h2>
33+ <div id="article-list" class="space-y-3">
14 {{range .Articles}}34 {{range .Articles}}
15 {{template "article-card.html" .}}35 {{template "article-card.html" .}}
16 {{else}}36 {{else}}
@@ -18,22 +38,27 @@
18 {{end}}38 {{end}}
19 </div>39 </div>
20 40
21- {{if .Articles}}41+ {{if .HasMore}}
22- <div class="mt-4 text-center">42+ <div class="text-center py-4">
23- <a href="/articles" class="text-sm text-spot-secondary hover:text-spot-green transition">View all articles &rarr;</a>43+ <button hx-get="/dashboard?offset={{.NextOffset}}"
44+ hx-target="#article-list" hx-swap="beforeend"
45+ hx-select="article"
46+ class="text-sm text-spot-secondary hover:text-spot-green transition">
47+ Load more
48+ </button>
24 </div>49 </div>
25 {{end}}50 {{end}}
26 </div>51 </div>
27 52
28 <div class="space-y-8">53 <div class="space-y-8">
29- {{if .Trending}}54+ {{if .PersonalTrending}}
30 <div>55 <div>
31 <div class="flex items-center justify-between mb-4">56 <div class="flex items-center justify-between mb-4">
32- <h2 class="text-lg font-semibold text-spot-text">Trending</h2>57+ <h2 class="text-lg font-semibold text-spot-text">Trending in your network</h2>
33- <a href="/trending" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>58+ <a href="/trending?scope=for-me" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>
34 </div>59 </div>
35 <div class="space-y-3">60 <div class="space-y-3">
36- {{range .Trending}}61+ {{range .PersonalTrending}}
37 <a href="/articles/{{.ArticleID}}" class="block bg-spot-surface rounded-xl p-4 hover:bg-spot-hover-50 transition">62 <a href="/articles/{{.ArticleID}}" class="block bg-spot-surface rounded-xl p-4 hover:bg-spot-hover-50 transition">
38 <div class="font-bold text-sm text-spot-text leading-tight">{{.Title}}</div>63 <div class="font-bold text-sm text-spot-text leading-tight">{{.Title}}</div>
39 <div class="flex items-center gap-2 mt-1 text-xs text-spot-secondary">64 <div class="flex items-center gap-2 mt-1 text-xs text-spot-secondary">
@@ -52,10 +77,7 @@
52 77
53 {{if .PeopleRecommendations}}78 {{if .PeopleRecommendations}}
54 <div>79 <div>
55- <div class="flex items-center justify-between mb-4">80+ <h2 class="text-lg font-semibold text-spot-text mb-4">Similar readers</h2>
56- <h2 class="text-lg font-semibold text-spot-text">Similar readers</h2>
57- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>
58- </div>
59 <div class="space-y-3">81 <div class="space-y-3">
60 {{range .PeopleRecommendations}}82 {{range .PeopleRecommendations}}
61 <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">83 <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">
@@ -73,10 +95,7 @@
73 95
74 {{if .FeedRecommendations}}96 {{if .FeedRecommendations}}
75 <div>97 <div>
76- <div class="flex items-center justify-between mb-4">98+ <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2>
77- <h2 class="text-lg font-semibold text-spot-text">Recommended feeds</h2>
78- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>
79- </div>
80 <div class="space-y-3">99 <div class="space-y-3">
81 {{range .FeedRecommendations}}100 {{range .FeedRecommendations}}
82 {{template "recommendation-card.html" .}}101 {{template "recommendation-card.html" .}}
deleted internal/tmpl/discover.html +0 -78
deleted file mode 100644
@@ -1,78 +0,0 @@
1-{{define "discover.html"}}
2-<div class="flex items-center justify-between mb-2">
3- <h1 class="text-2xl font-bold text-spot-text">Discover</h1>
4- <div></div>
5-</div>
6-<p class="text-sm text-spot-secondary mb-6">Recommended feeds, articles, and readers based on your activity.</p>
7-
8-{{if .ArticleRecommendations}}
9-<div class="mb-8">
10- <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended articles</h2>
11- <div class="space-y-3">
12- {{range .ArticleRecommendations}}
13- <a href="/articles/{{.ArticleID}}" class="block bg-spot-surface rounded-xl p-4 hover:bg-spot-hover-50 transition">
14- <div class="font-bold text-sm text-spot-text leading-tight">{{.Title}}</div>
15- <div class="flex items-center gap-2 mt-1 text-xs text-spot-secondary">
16- {{if .Author}}<span>{{.Author}}</span>{{end}}
17- <span class="text-spot-muted">{{.FeedTitle}}</span>
18- {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2"}}</span>{{end}}
19- </div>
20- {{if .Summary}}<p class="text-xs text-spot-secondary mt-2 line-clamp-2">{{.Summary}}</p>{{end}}
21- </a>
22- {{end}}
23- </div>
24-</div>
25-{{end}}
26-
27-<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
28- <div>
29- <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2>
30- <div class="space-y-3">
31- {{range .FeedRecommendations}}
32- {{template "recommendation-card.html" .}}
33- {{else}}
34- <p class="text-sm text-spot-secondary">Subscribe to some feeds first to get recommendations.</p>
35- {{end}}
36- </div>
37- </div>
38-
39- <div>
40- <h2 class="text-lg font-semibold text-spot-text mb-4">Similar readers</h2>
41- <div class="space-y-3">
42- {{range .PeopleRecommendations}}
43- <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">
44- {{if .avatar_url}}<img src="{{.avatar_url}}" class="w-10 h-10 rounded-full">{{end}}
45- <div class="min-w-0 flex-1">
46- <a href="/profile/{{.handle}}" class="font-bold text-spot-text hover:text-spot-green transition">@{{.handle}}</a>
47- {{if .display_name}}<div class="text-sm text-spot-secondary">{{.display_name}}</div>{{end}}
48- </div>
49- <span class="text-xs text-spot-secondary">{{.common_feeds}} shared feeds</span>
50- </div>
51- {{else}}
52- <p class="text-sm text-spot-secondary">Not enough data yet. Keep reading!</p>
53- {{end}}
54- </div>
55- </div>
56-</div>
57-
58-<div class="mt-8">
59- <h2 class="text-lg font-semibold text-spot-text mb-4">Browse all feeds</h2>
60- <div class="bg-spot-surface rounded-xl divide-y divide-spot-divider">
61- {{range .PopularFeeds}}
62- <div class="px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">
63- <div class="min-w-0">
64- <div class="font-bold text-spot-text">{{if .Title.Valid}}{{.Title.String}}{{else}}{{.FeedURL}}{{end}}</div>
65- {{if .Description.Valid}}<p class="text-sm text-spot-secondary truncate">{{.Description.String}}</p>{{end}}
66- </div>
67- <div class="flex items-center gap-3 shrink-0">
68- <span class="text-xs text-spot-secondary">{{.SubscriberCount}} subscribers</span>
69- <button hx-post="/feeds" hx-vals='{"feed_url": "{{.FeedURL}}"}' hx-target="this" hx-swap="outerHTML"
70- class="text-xs bg-spot-green/20 text-spot-green px-4 py-1.5 rounded-pill font-bold uppercase tracking-button hover:bg-spot-green hover:text-white transition">Subscribe</button>
71- </div>
72- </div>
73- {{else}}
74- <div class="px-4 py-8 text-center text-spot-secondary">No feeds in the directory yet.</div>
75- {{end}}
76- </div>
77-</div>
78-{{end}}
deleted file mode 100644
@@ -1,78 +0,0 @@
1-{{define "discover.html"}}
2-<div class="flex items-center justify-between mb-2">
3- <h1 class="text-2xl font-bold text-spot-text">Discover</h1>
4- <div></div>
5-</div>
6-<p class="text-sm text-spot-secondary mb-6">Recommended feeds, articles, and readers based on your activity.</p>
7-
8-{{if .ArticleRecommendations}}
9-<div class="mb-8">
10- <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended articles</h2>
11- <div class="space-y-3">
12- {{range .ArticleRecommendations}}
13- <a href="/articles/{{.ArticleID}}" class="block bg-spot-surface rounded-xl p-4 hover:bg-spot-hover-50 transition">
14- <div class="font-bold text-sm text-spot-text leading-tight">{{.Title}}</div>
15- <div class="flex items-center gap-2 mt-1 text-xs text-spot-secondary">
16- {{if .Author}}<span>{{.Author}}</span>{{end}}
17- <span class="text-spot-muted">{{.FeedTitle}}</span>
18- {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2"}}</span>{{end}}
19- </div>
20- {{if .Summary}}<p class="text-xs text-spot-secondary mt-2 line-clamp-2">{{.Summary}}</p>{{end}}
21- </a>
22- {{end}}
23- </div>
24-</div>
25-{{end}}
26-
27-<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
28- <div>
29- <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2>
30- <div class="space-y-3">
31- {{range .FeedRecommendations}}
32- {{template "recommendation-card.html" .}}
33- {{else}}
34- <p class="text-sm text-spot-secondary">Subscribe to some feeds first to get recommendations.</p>
35- {{end}}
36- </div>
37- </div>
38-
39- <div>
40- <h2 class="text-lg font-semibold text-spot-text mb-4">Similar readers</h2>
41- <div class="space-y-3">
42- {{range .PeopleRecommendations}}
43- <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">
44- {{if .avatar_url}}<img src="{{.avatar_url}}" class="w-10 h-10 rounded-full">{{end}}
45- <div class="min-w-0 flex-1">
46- <a href="/profile/{{.handle}}" class="font-bold text-spot-text hover:text-spot-green transition">@{{.handle}}</a>
47- {{if .display_name}}<div class="text-sm text-spot-secondary">{{.display_name}}</div>{{end}}
48- </div>
49- <span class="text-xs text-spot-secondary">{{.common_feeds}} shared feeds</span>
50- </div>
51- {{else}}
52- <p class="text-sm text-spot-secondary">Not enough data yet. Keep reading!</p>
53- {{end}}
54- </div>
55- </div>
56-</div>
57-
58-<div class="mt-8">
59- <h2 class="text-lg font-semibold text-spot-text mb-4">Browse all feeds</h2>
60- <div class="bg-spot-surface rounded-xl divide-y divide-spot-divider">
61- {{range .PopularFeeds}}
62- <div class="px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">
63- <div class="min-w-0">
64- <div class="font-bold text-spot-text">{{if .Title.Valid}}{{.Title.String}}{{else}}{{.FeedURL}}{{end}}</div>
65- {{if .Description.Valid}}<p class="text-sm text-spot-secondary truncate">{{.Description.String}}</p>{{end}}
66- </div>
67- <div class="flex items-center gap-3 shrink-0">
68- <span class="text-xs text-spot-secondary">{{.SubscriberCount}} subscribers</span>
69- <button hx-post="/feeds" hx-vals='{"feed_url": "{{.FeedURL}}"}' hx-target="this" hx-swap="outerHTML"
70- class="text-xs bg-spot-green/20 text-spot-green px-4 py-1.5 rounded-pill font-bold uppercase tracking-button hover:bg-spot-green hover:text-white transition">Subscribe</button>
71- </div>
72- </div>
73- {{else}}
74- <div class="px-4 py-8 text-center text-spot-secondary">No feeds in the directory yet.</div>
75- {{end}}
76- </div>
77-</div>
78-{{end}}
modified internal/tmpl/feeds.html +29 -21
@@ -29,6 +29,17 @@
2929 </div>
3030 {{end}}
3131
32+{{if .FeedRecommendations}}
33+<div class="mb-6">
34+ <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide mb-3">Recommended feeds</h2>
35+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
36+ {{range .FeedRecommendations}}
37+ {{template "recommendation-card.html" .}}
38+ {{end}}
39+ </div>
40+</div>
41+{{end}}
42+
3243 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
3344 <div class="lg:col-span-2">
3445 {{if .Categories}}
@@ -64,9 +75,12 @@
6475 <option value="1440" {{if eq .FetchInterval 1440}}selected{{end}}>24h</option>
6576 </select>
6677 </form>
67- <button hx-delete="/feeds/{{.FeedURL}}" hx-target="closest .px-5" hx-swap="outerHTML swap:0.3s"
68- hx-confirm="Unsubscribe from this feed?"
69- class="text-sm text-spot-secondary hover:text-spot-red transition">Unsubscribe</button>
78+ <form hx-delete="/feeds/remove" hx-target="closest .px-5" hx-swap="outerHTML swap:0.3s"
79+ hx-confirm="Unsubscribe from this feed?" class="inline">
80+ {{csrfInput $.CSRFToken}}
81+ <input type="hidden" name="url" value="{{.FeedURL}}">
82+ <button type="submit" class="text-sm text-spot-secondary hover:text-spot-red transition">Unsubscribe</button>
83+ </form>
7084 </div>
7185 </div>
7286 {{else}}
@@ -75,6 +89,17 @@
7589 </div>
7690 {{end}}
7791 </div>
92+
93+ {{if .HasMore}}
94+ <div class="text-center py-4">
95+ <button hx-get="/feeds?offset={{.NextOffset}}{{if .Category}}&category={{.Category}}{{end}}"
96+ hx-target="#feed-list" hx-swap="beforeend"
97+ hx-select=".px-5"
98+ class="text-sm text-spot-secondary hover:text-spot-green transition">
99+ Load more
100+ </button>
101+ </div>
102+ {{end}}
78103 </div>
79104
80105 <div class="space-y-6">
@@ -97,26 +122,9 @@
97122 </form>
98123 </div>
99124
100- {{if .FeedRecommendations}}
101- <div>
102- <div class="flex items-center justify-between mb-4">
103- <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide">Recommended feeds</h2>
104- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>
105- </div>
106- <div class="space-y-3">
107- {{range .FeedRecommendations}}
108- {{template "recommendation-card.html" .}}
109- {{end}}
110- </div>
111- </div>
112- {{end}}
113-
114125 {{if .PeopleRecommendations}}
115126 <div>
116- <div class="flex items-center justify-between mb-4">
117- <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide">Similar readers</h2>
118- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>
119- </div>
127+ <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide mb-4">Similar readers</h2>
120128 <div class="space-y-3">
121129 {{range .PeopleRecommendations}}
122130 <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">
@@ -29,6 +29,17 @@
29 </div>29 </div>
30 {{end}}30 {{end}}
31 31
32+{{if .FeedRecommendations}}
33+<div class="mb-6">
34+ <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide mb-3">Recommended feeds</h2>
35+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
36+ {{range .FeedRecommendations}}
37+ {{template "recommendation-card.html" .}}
38+ {{end}}
39+ </div>
40+</div>
41+{{end}}
42+
32 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">43 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
33 <div class="lg:col-span-2">44 <div class="lg:col-span-2">
34 {{if .Categories}}45 {{if .Categories}}
@@ -64,9 +75,12 @@
64 <option value="1440" {{if eq .FetchInterval 1440}}selected{{end}}>24h</option>75 <option value="1440" {{if eq .FetchInterval 1440}}selected{{end}}>24h</option>
65 </select>76 </select>
66 </form>77 </form>
67- <button hx-delete="/feeds/{{.FeedURL}}" hx-target="closest .px-5" hx-swap="outerHTML swap:0.3s"78+ <form hx-delete="/feeds/remove" hx-target="closest .px-5" hx-swap="outerHTML swap:0.3s"
68- hx-confirm="Unsubscribe from this feed?"79+ hx-confirm="Unsubscribe from this feed?" class="inline">
69- class="text-sm text-spot-secondary hover:text-spot-red transition">Unsubscribe</button>80+ {{csrfInput $.CSRFToken}}
81+ <input type="hidden" name="url" value="{{.FeedURL}}">
82+ <button type="submit" class="text-sm text-spot-secondary hover:text-spot-red transition">Unsubscribe</button>
83+ </form>
70 </div>84 </div>
71 </div>85 </div>
72 {{else}}86 {{else}}
@@ -75,6 +89,17 @@
75 </div>89 </div>
76 {{end}}90 {{end}}
77 </div>91 </div>
92+
93+ {{if .HasMore}}
94+ <div class="text-center py-4">
95+ <button hx-get="/feeds?offset={{.NextOffset}}{{if .Category}}&category={{.Category}}{{end}}"
96+ hx-target="#feed-list" hx-swap="beforeend"
97+ hx-select=".px-5"
98+ class="text-sm text-spot-secondary hover:text-spot-green transition">
99+ Load more
100+ </button>
101+ </div>
102+ {{end}}
78 </div>103 </div>
79 104
80 <div class="space-y-6">105 <div class="space-y-6">
@@ -97,26 +122,9 @@
97 </form>122 </form>
98 </div>123 </div>
99 124
100- {{if .FeedRecommendations}}
101- <div>
102- <div class="flex items-center justify-between mb-4">
103- <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide">Recommended feeds</h2>
104- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>
105- </div>
106- <div class="space-y-3">
107- {{range .FeedRecommendations}}
108- {{template "recommendation-card.html" .}}
109- {{end}}
110- </div>
111- </div>
112- {{end}}
113-
114 {{if .PeopleRecommendations}}125 {{if .PeopleRecommendations}}
115 <div>126 <div>
116- <div class="flex items-center justify-between mb-4">127+ <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide mb-4">Similar readers</h2>
117- <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide">Similar readers</h2>
118- <a href="/discover" class="text-xs text-spot-secondary hover:text-spot-green uppercase tracking-button transition">See all</a>
119- </div>
120 <div class="space-y-3">128 <div class="space-y-3">
121 {{range .PeopleRecommendations}}129 {{range .PeopleRecommendations}}
122 <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">130 <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">
added internal/tmpl/library.html +66 -0
new file mode 100644
@@ -0,0 +1,66 @@
1+{{define "library.html"}}
2+<div class="flex items-center justify-between mb-2">
3+ <h1 class="text-2xl font-bold text-spot-text">Library</h1>
4+ <div></div>
5+</div>
6+<p class="text-sm text-spot-secondary mb-6">Your liked articles and annotations.</p>
7+
8+<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
9+ <div>
10+ <h2 class="text-lg font-semibold text-spot-text mb-4">Liked articles</h2>
11+ <div id="liked-list" class="space-y-3">
12+ {{range .Articles}}
13+ {{template "article-card.html" .}}
14+ {{else}}
15+ <div class="text-center text-spot-secondary py-12">
16+ <p>No liked articles yet.</p>
17+ <p class="text-sm mt-1">Like articles to save them here.</p>
18+ </div>
19+ {{end}}
20+ </div>
21+
22+ {{if .LikedHasMore}}
23+ <div class="text-center py-4">
24+ <button hx-get="/library?liked_offset={{.NextLiked}}"
25+ hx-target="#liked-list" hx-swap="beforeend"
26+ hx-select="article"
27+ class="text-sm text-spot-secondary hover:text-spot-green transition">
28+ Load more
29+ </button>
30+ </div>
31+ {{end}}
32+ </div>
33+
34+ <div>
35+ <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2>
36+ <div id="annotations-list" class="space-y-3">
37+ {{range .Annotations}}
38+ <div class="bg-spot-surface rounded-xl p-4">
39+ {{if .ArticleURL}}
40+ <div class="text-xs text-spot-muted mb-2">
41+ <a href="{{.ArticleURL}}" class="text-spot-secondary hover:text-spot-green transition truncate block">{{.ArticleURL}}</a>
42+ </div>
43+ {{end}}
44+ {{template "annotation-card.html" .}}
45+ </div>
46+ {{else}}
47+ <div class="text-center text-spot-secondary py-12">
48+ <p>No annotations yet.</p>
49+ <p class="text-sm mt-1">Highlight and annotate articles as you read.</p>
50+ </div>
51+ {{end}}
52+ </div>
53+
54+ {{if .AnnotHasMore}}
55+ <div class="text-center py-4">
56+ <button hx-get="/library?annot_offset={{.NextAnnot}}"
57+ hx-target="#annotations-list" hx-swap="beforeend"
58+ hx-select=".bg-spot-surface"
59+ class="text-sm text-spot-secondary hover:text-spot-green transition">
60+ Load more
61+ </button>
62+ </div>
63+ {{end}}
64+ </div>
65+</div>
66+{{end}}
new file mode 100644
@@ -0,0 +1,66 @@
1+{{define "library.html"}}
2+<div class="flex items-center justify-between mb-2">
3+ <h1 class="text-2xl font-bold text-spot-text">Library</h1>
4+ <div></div>
5+</div>
6+<p class="text-sm text-spot-secondary mb-6">Your liked articles and annotations.</p>
7+
8+<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
9+ <div>
10+ <h2 class="text-lg font-semibold text-spot-text mb-4">Liked articles</h2>
11+ <div id="liked-list" class="space-y-3">
12+ {{range .Articles}}
13+ {{template "article-card.html" .}}
14+ {{else}}
15+ <div class="text-center text-spot-secondary py-12">
16+ <p>No liked articles yet.</p>
17+ <p class="text-sm mt-1">Like articles to save them here.</p>
18+ </div>
19+ {{end}}
20+ </div>
21+
22+ {{if .LikedHasMore}}
23+ <div class="text-center py-4">
24+ <button hx-get="/library?liked_offset={{.NextLiked}}"
25+ hx-target="#liked-list" hx-swap="beforeend"
26+ hx-select="article"
27+ class="text-sm text-spot-secondary hover:text-spot-green transition">
28+ Load more
29+ </button>
30+ </div>
31+ {{end}}
32+ </div>
33+
34+ <div>
35+ <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2>
36+ <div id="annotations-list" class="space-y-3">
37+ {{range .Annotations}}
38+ <div class="bg-spot-surface rounded-xl p-4">
39+ {{if .ArticleURL}}
40+ <div class="text-xs text-spot-muted mb-2">
41+ <a href="{{.ArticleURL}}" class="text-spot-secondary hover:text-spot-green transition truncate block">{{.ArticleURL}}</a>
42+ </div>
43+ {{end}}
44+ {{template "annotation-card.html" .}}
45+ </div>
46+ {{else}}
47+ <div class="text-center text-spot-secondary py-12">
48+ <p>No annotations yet.</p>
49+ <p class="text-sm mt-1">Highlight and annotate articles as you read.</p>
50+ </div>
51+ {{end}}
52+ </div>
53+
54+ {{if .AnnotHasMore}}
55+ <div class="text-center py-4">
56+ <button hx-get="/library?annot_offset={{.NextAnnot}}"
57+ hx-target="#annotations-list" hx-swap="beforeend"
58+ hx-select=".bg-spot-surface"
59+ class="text-sm text-spot-secondary hover:text-spot-green transition">
60+ Load more
61+ </button>
62+ </div>
63+ {{end}}
64+ </div>
65+</div>
66+{{end}}