Implement pagination and social trending featuresUnverified
e9f75c1 parent: 2c8faf0 modified
internal/db/social.go +69 -0 | @@ -190,6 +190,45 @@ type TrendingItem struct { | ||
| 190 | 190 | 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 | 232 | func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, offset int) ([]*TrendingItem, error) { |
| 194 | 233 | rows, err := db.QueryContext(ctx, fmt.Sprintf(` |
| 195 | 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 | 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 | +} | |
| @@ -190,6 +190,45 @@ type TrendingItem struct { | |||
| 190 | AnnotationCount int | 190 | 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 ( | ||
| 10 | 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 | 13 | func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) { |
| 25 | 14 | user := currentUser(r) |
| 26 | 15 | 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) { | ||
| 37 | 37 | user := currentUser(r) |
| 38 | 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 | 47 | if err != nil { |
| 42 | 48 | s.logger.Error("failed to list articles", "error", err) |
| 43 | 49 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 44 | 50 | return |
| 45 | 51 | } |
| 46 | 52 | |
| 53 | + hasMore := len(articles) > pageLimit | |
| 54 | + if hasMore { | |
| 55 | + articles = articles[:pageLimit] | |
| 56 | + } | |
| 57 | + | |
| 47 | 58 | 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, | |
| 51 | 64 | }) |
| 52 | 65 | } |
| 53 | 66 | |
| @@ -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 | return | 50 | 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 | ||
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "net/http" |
| 5 | + "strconv" | |
| 5 | 6 | "time" |
| 6 | 7 | ) |
| 7 | 8 | |
| @@ -10,19 +11,36 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | ||
| 10 | 11 | |
| 11 | 12 | unreadCount, _ := s.db.GetUnreadCount(r.Context(), user.DID, "") |
| 12 | 13 | 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) | |
| 15 | 28 | peopleRecs, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 5) |
| 29 | + feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 5) | |
| 30 | + | |
| 16 | 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 | 34 | s.render(w, r, "dashboard.html", map[string]any{ |
| 20 | 35 | "User": user, |
| 21 | 36 | "UnreadCount": unreadCount, |
| 22 | 37 | "SubscriptionCount": subCount, |
| 23 | 38 | "Articles": articles, |
| 39 | + "ArticleRecommendations": articleRecs, | |
| 24 | 40 | "FeedRecommendations": feedRecs, |
| 25 | 41 | "PeopleRecommendations": peopleRecs, |
| 26 | - "Trending": trending, | |
| 42 | + "PersonalTrending": personalTrending, | |
| 43 | + "HasMore": hasMore, | |
| 44 | + "NextOffset": offset + pageLimit, | |
| 27 | 45 | }) |
| 28 | 46 | } |
| @@ -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 ( | ||
| 17 | 17 | func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) { |
| 18 | 18 | user := currentUser(r) |
| 19 | 19 | 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) | |
| 22 | 34 | feedRecs, _ := s.db.GetFeedRecommendations(r.Context(), user.DID, 10) |
| 23 | 35 | peopleRecs, _ := s.db.GetPeopleRecommendations(r.Context(), user.DID, 5) |
| 24 | 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 | 52 | "FeedRecommendations": feedRecs, |
| 41 | 53 | "PeopleRecommendations": peopleRecs, |
| 42 | 54 | "DeadFeeds": deadFeeds, |
| 55 | + "HasMore": hasMore, | |
| 56 | + "NextOffset": offset + pageLimit, | |
| 43 | 57 | }) |
| 44 | 58 | } |
| 45 | 59 | |
| @@ -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() { | ||
| 124 | 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 | 127 | s.router.Route("/profile", func(r chi.Router) { |
| 135 | 128 | r.Use(s.requireAuth) |
| 136 | 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 | 133 | r.Use(s.requireAuth) |
| 141 | - r.Get("/", s.handleAnnotations) | |
| 134 | + r.Get("/", s.handleLibrary) | |
| 142 | 135 | r.Post("/create", s.handleCreateAnnotation) |
| 143 | 136 | }) |
| 144 | 137 | |
| @@ -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 | ||
modified
internal/server/trending_handler.go +33 -3 | @@ -2,15 +2,45 @@ package server | ||
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "net/http" |
| 5 | + "strconv" | |
| 5 | 6 | "time" |
| 7 | + | |
| 8 | + "pkg.rbrt.fr/glean/internal/db" | |
| 6 | 9 | ) |
| 7 | 10 | |
| 8 | 11 | func (s *Server) handleTrending(w http.ResponseWriter, r *http.Request) { |
| 9 | 12 | user := currentUser(r) |
| 13 | + | |
| 14 | + scope := r.URL.Query().Get("scope") | |
| 15 | + if scope != "all" { | |
| 16 | + scope = "for-me" | |
| 17 | + } | |
| 18 | + | |
| 19 | + offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) | |
| 20 | + if offset < 0 { | |
| 21 | + offset = 0 | |
| 22 | + } | |
| 23 | + pageLimit := 25 | |
| 24 | + | |
| 10 | 25 | since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339) |
| 11 | - articles, _ := s.db.ListTrendingArticles(r.Context(), since, 25, 0) | |
| 26 | + | |
| 27 | + var trending []*db.TrendingItem | |
| 28 | + if scope == "for-me" { | |
| 29 | + trending, _ = s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, pageLimit+1, offset) | |
| 30 | + } else { | |
| 31 | + trending, _ = s.db.ListTrendingArticles(r.Context(), since, pageLimit+1, offset) | |
| 32 | + } | |
| 33 | + | |
| 34 | + hasMore := len(trending) > pageLimit | |
| 35 | + if hasMore { | |
| 36 | + trending = trending[:pageLimit] | |
| 37 | + } | |
| 38 | + | |
| 12 | 39 | s.render(w, r, "trending.html", map[string]any{ |
| 13 | - "User": user, | |
| 14 | - "Trending": articles, | |
| 40 | + "User": user, | |
| 41 | + "Trending": trending, | |
| 42 | + "Scope": scope, | |
| 43 | + "HasMore": hasMore, | |
| 44 | + "NextOffset": offset + pageLimit, | |
| 15 | 45 | }) |
| 16 | 46 | } |
| @@ -2,15 +2,45 @@ package server | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "net/http" | 4 | "net/http" |
| 5 | + "strconv" | ||
| 5 | "time" | 6 | "time" |
| 7 | + | ||
| 8 | + "pkg.rbrt.fr/glean/internal/db" | ||
| 6 | ) | 9 | ) |
| 7 | 10 | ||
| 8 | func (s *Server) handleTrending(w http.ResponseWriter, r *http.Request) { | 11 | func (s *Server) handleTrending(w http.ResponseWriter, r *http.Request) { |
| 9 | user := currentUser(r) | 12 | user := currentUser(r) |
| 13 | + | ||
| 14 | + scope := r.URL.Query().Get("scope") | ||
| 15 | + if scope != "all" { | ||
| 16 | + scope = "for-me" | ||
| 17 | + } | ||
| 18 | + | ||
| 19 | + offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) | ||
| 20 | + if offset < 0 { | ||
| 21 | + offset = 0 | ||
| 22 | + } | ||
| 23 | + pageLimit := 25 | ||
| 24 | + | ||
| 10 | since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339) | 25 | since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339) |
| 11 | - articles, _ := s.db.ListTrendingArticles(r.Context(), since, 25, 0) | 26 | + |
| 27 | + var trending []*db.TrendingItem | ||
| 28 | + if scope == "for-me" { | ||
| 29 | + trending, _ = s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, pageLimit+1, offset) | ||
| 30 | + } else { | ||
| 31 | + trending, _ = s.db.ListTrendingArticles(r.Context(), since, pageLimit+1, offset) | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + hasMore := len(trending) > pageLimit | ||
| 35 | + if hasMore { | ||
| 36 | + trending = trending[:pageLimit] | ||
| 37 | + } | ||
| 38 | + | ||
| 12 | s.render(w, r, "trending.html", map[string]any{ | 39 | s.render(w, r, "trending.html", map[string]any{ |
| 13 | - "User": user, | 40 | + "User": user, |
| 14 | - "Trending": articles, | 41 | + "Trending": trending, |
| 42 | + "Scope": scope, | ||
| 43 | + "HasMore": hasMore, | ||
| 44 | + "NextOffset": offset + pageLimit, | ||
| 15 | }) | 45 | }) |
| 16 | } | 46 | } |
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 @@ | ||
| 66 | 66 | <section> |
| 67 | 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 | 70 | hx-on::after-request="this.reset()" |
| 71 | 71 | class="bg-spot-surface rounded-xl shadow-spot p-4 mb-4 space-y-3"> |
| 72 | 72 | {{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 @@ | ||
| 38 | 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 | 39 | Trending |
| 40 | 40 | </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"}}"> | |
| 46 | 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 | - Annotations | |
| 43 | + Library | |
| 48 | 44 | </a> |
| 49 | 45 | </nav> |
| 50 | 46 | <div class="mt-auto pt-4 border-t border-spot-divider-30"> |
| @@ -81,10 +77,6 @@ | ||
| 81 | 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 | 78 | Trending |
| 83 | 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 | 80 | </div> |
| 89 | 81 | </nav> |
| 90 | 82 | {{end}} |
| @@ -121,10 +113,6 @@ | ||
| 121 | 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 | 114 | Trending |
| 123 | 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 | 116 | </div> |
| 129 | 117 | <div class="flex flex-col gap-1.5"> |
| 130 | 118 | <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Library</div> |
| @@ -136,9 +124,9 @@ | ||
| 136 | 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 | 125 | Feeds |
| 138 | 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 | 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 | - Annotations | |
| 129 | + Library | |
| 142 | 130 | </a> |
| 143 | 131 | </div> |
| 144 | 132 | <div class="flex flex-col gap-1.5"> |
| @@ -164,13 +152,12 @@ | ||
| 164 | 152 | <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Browse</div> |
| 165 | 153 | <a href="/dashboard" class="text-xs text-spot-secondary hover:text-spot-text transition">Dashboard</a> |
| 166 | 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 | 155 | </div> |
| 169 | 156 | <div class="flex flex-col gap-1.5"> |
| 170 | 157 | <div class="text-spot-text font-bold text-xs uppercase tracking-wide mb-1">Library</div> |
| 171 | 158 | <a href="/articles" class="text-xs text-spot-secondary hover:text-spot-text transition">Articles</a> |
| 172 | 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 | 161 | </div> |
| 175 | 162 | </div> |
| 176 | 163 | <div class="flex items-center justify-between"> |
| @@ -218,7 +205,7 @@ | ||
| 218 | 205 | if (e.key === 'a') window.location.href = '/articles'; |
| 219 | 206 | if (e.key === 'f') window.location.href = '/feeds'; |
| 220 | 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 | 210 | </script> |
| 224 | 211 | </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 | Trending | 39 | 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 | - Annotations | 43 | + 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 | Trending | 78 | 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 | Trending | 114 | 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 | Feeds | 125 | 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 | - Annotations | 129 | + 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 @@ | ||
| 6 | 6 | <span class="text-spot-secondary"><strong class="text-spot-text">{{.SubscriptionCount}}</strong> feeds</span> |
| 7 | 7 | </div> |
| 8 | 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 | 11 | <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
| 12 | 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 | 34 | {{range .Articles}} |
| 15 | 35 | {{template "article-card.html" .}} |
| 16 | 36 | {{else}} |
| @@ -18,22 +38,27 @@ | ||
| 18 | 38 | {{end}} |
| 19 | 39 | </div> |
| 20 | 40 | |
| 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 →</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> | |
| 24 | 49 | </div> |
| 25 | 50 | {{end}} |
| 26 | 51 | </div> |
| 27 | 52 | |
| 28 | 53 | <div class="space-y-8"> |
| 29 | - {{if .Trending}} | |
| 54 | + {{if .PersonalTrending}} | |
| 30 | 55 | <div> |
| 31 | 56 | <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> | |
| 34 | 59 | </div> |
| 35 | 60 | <div class="space-y-3"> |
| 36 | - {{range .Trending}} | |
| 61 | + {{range .PersonalTrending}} | |
| 37 | 62 | <a href="/articles/{{.ArticleID}}" class="block bg-spot-surface rounded-xl p-4 hover:bg-spot-hover-50 transition"> |
| 38 | 63 | <div class="font-bold text-sm text-spot-text leading-tight">{{.Title}}</div> |
| 39 | 64 | <div class="flex items-center gap-2 mt-1 text-xs text-spot-secondary"> |
| @@ -52,10 +77,7 @@ | ||
| 52 | 77 | |
| 53 | 78 | {{if .PeopleRecommendations}} |
| 54 | 79 | <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> | |
| 59 | 81 | <div class="space-y-3"> |
| 60 | 82 | {{range .PeopleRecommendations}} |
| 61 | 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 | 96 | {{if .FeedRecommendations}} |
| 75 | 97 | <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> | |
| 80 | 99 | <div class="space-y-3"> |
| 81 | 100 | {{range .FeedRecommendations}} |
| 82 | 101 | {{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 →</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 @@ | ||
| 29 | 29 | </div> |
| 30 | 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 | 43 | <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
| 33 | 44 | <div class="lg:col-span-2"> |
| 34 | 45 | {{if .Categories}} |
| @@ -64,9 +75,12 @@ | ||
| 64 | 75 | <option value="1440" {{if eq .FetchInterval 1440}}selected{{end}}>24h</option> |
| 65 | 76 | </select> |
| 66 | 77 | </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> | |
| 70 | 84 | </div> |
| 71 | 85 | </div> |
| 72 | 86 | {{else}} |
| @@ -75,6 +89,17 @@ | ||
| 75 | 89 | </div> |
| 76 | 90 | {{end}} |
| 77 | 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 | 103 | </div> |
| 79 | 104 | |
| 80 | 105 | <div class="space-y-6"> |
| @@ -97,26 +122,9 @@ | ||
| 97 | 122 | </form> |
| 98 | 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 | 125 | {{if .PeopleRecommendations}} |
| 115 | 126 | <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> | |
| 120 | 128 | <div class="space-y-3"> |
| 121 | 129 | {{range .PeopleRecommendations}} |
| 122 | 130 | <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}} | ||
modified
internal/tmpl/trending.html +21 -3 | @@ -1,11 +1,18 @@ | ||
| 1 | 1 | {{define "trending.html"}} |
| 2 | 2 | <div class="flex items-center justify-between mb-2"> |
| 3 | 3 | <h1 class="text-2xl font-bold text-spot-text">Trending</h1> |
| 4 | - <div></div> | |
| 4 | + <div class="flex gap-2"> | |
| 5 | + <a href="/trending?scope=all" | |
| 6 | + class="text-sm px-4 py-1.5 rounded-full font-bold {{if ne .Scope "for-me"}}bg-spot-active-pill-bg text-spot-active-pill-text{{else}}bg-spot-hover text-spot-secondary hover:text-spot-text{{end}} transition">All</a> | |
| 7 | + <a href="/trending?scope=for-me" | |
| 8 | + class="text-sm px-4 py-1.5 rounded-full font-bold {{if eq .Scope "for-me"}}bg-spot-active-pill-bg text-spot-active-pill-text{{else}}bg-spot-hover text-spot-secondary hover:text-spot-text{{end}} transition">For me</a> | |
| 9 | + </div> | |
| 5 | 10 | </div> |
| 6 | -<p class="text-sm text-spot-secondary mb-6">Most liked and discussed articles across Glean.</p> | |
| 11 | +<p class="text-sm text-spot-secondary mb-6"> | |
| 12 | + {{if eq .Scope "for-me"}}Trending articles from readers with similar tastes.{{else}}Most liked and discussed articles across Glean.{{end}} | |
| 13 | +</p> | |
| 7 | 14 | |
| 8 | -<div class="space-y-3"> | |
| 15 | +<div id="trending-list" class="space-y-3"> | |
| 9 | 16 | {{range $i, $t := .Trending}} |
| 10 | 17 | <article class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot"> |
| 11 | 18 | <div class="flex items-start justify-between gap-4"> |
| @@ -29,4 +36,15 @@ | ||
| 29 | 36 | <div class="text-center text-spot-secondary py-12">No trending articles yet. Start liking and annotating!</div> |
| 30 | 37 | {{end}} |
| 31 | 38 | </div> |
| 39 | + | |
| 40 | +{{if .HasMore}} | |
| 41 | +<div class="text-center py-4"> | |
| 42 | + <button hx-get="/trending?scope={{.Scope}}&offset={{.NextOffset}}" | |
| 43 | + hx-target="#trending-list" hx-swap="beforeend" | |
| 44 | + hx-select="article" | |
| 45 | + class="text-sm text-spot-secondary hover:text-spot-green transition"> | |
| 46 | + Load more | |
| 47 | + </button> | |
| 48 | +</div> | |
| 49 | +{{end}} | |
| 32 | 50 | {{end}} |
| @@ -1,11 +1,18 @@ | |||
| 1 | {{define "trending.html"}} | 1 | {{define "trending.html"}} |
| 2 | <div class="flex items-center justify-between mb-2"> | 2 | <div class="flex items-center justify-between mb-2"> |
| 3 | <h1 class="text-2xl font-bold text-spot-text">Trending</h1> | 3 | <h1 class="text-2xl font-bold text-spot-text">Trending</h1> |
| 4 | - <div></div> | 4 | + <div class="flex gap-2"> |
| 5 | + <a href="/trending?scope=all" | ||
| 6 | + class="text-sm px-4 py-1.5 rounded-full font-bold {{if ne .Scope "for-me"}}bg-spot-active-pill-bg text-spot-active-pill-text{{else}}bg-spot-hover text-spot-secondary hover:text-spot-text{{end}} transition">All</a> | ||
| 7 | + <a href="/trending?scope=for-me" | ||
| 8 | + class="text-sm px-4 py-1.5 rounded-full font-bold {{if eq .Scope "for-me"}}bg-spot-active-pill-bg text-spot-active-pill-text{{else}}bg-spot-hover text-spot-secondary hover:text-spot-text{{end}} transition">For me</a> | ||
| 9 | + </div> | ||
| 5 | </div> | 10 | </div> |
| 6 | -<p class="text-sm text-spot-secondary mb-6">Most liked and discussed articles across Glean.</p> | 11 | +<p class="text-sm text-spot-secondary mb-6"> |
| 12 | + {{if eq .Scope "for-me"}}Trending articles from readers with similar tastes.{{else}}Most liked and discussed articles across Glean.{{end}} | ||
| 13 | +</p> | ||
| 7 | 14 | ||
| 8 | -<div class="space-y-3"> | 15 | +<div id="trending-list" class="space-y-3"> |
| 9 | {{range $i, $t := .Trending}} | 16 | {{range $i, $t := .Trending}} |
| 10 | <article class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot"> | 17 | <article class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot"> |
| 11 | <div class="flex items-start justify-between gap-4"> | 18 | <div class="flex items-start justify-between gap-4"> |
| @@ -29,4 +36,15 @@ | |||
| 29 | <div class="text-center text-spot-secondary py-12">No trending articles yet. Start liking and annotating!</div> | 36 | <div class="text-center text-spot-secondary py-12">No trending articles yet. Start liking and annotating!</div> |
| 30 | {{end}} | 37 | {{end}} |
| 31 | </div> | 38 | </div> |
| 39 | + | ||
| 40 | +{{if .HasMore}} | ||
| 41 | +<div class="text-center py-4"> | ||
| 42 | + <button hx-get="/trending?scope={{.Scope}}&offset={{.NextOffset}}" | ||
| 43 | + hx-target="#trending-list" hx-swap="beforeend" | ||
| 44 | + hx-select="article" | ||
| 45 | + class="text-sm text-spot-secondary hover:text-spot-green transition"> | ||
| 46 | + Load more | ||
| 47 | + </button> | ||
| 48 | +</div> | ||
| 49 | +{{end}} | ||
| 32 | {{end}} | 50 | {{end}} |