Extract recommendations into separate endpoints/templates in dashboardUnverified
71d0cbb parent: ce55b7c modified
internal/server/dashboard_handler.go +124 -36 | @@ -4,6 +4,7 @@ import ( | ||
| 4 | 4 | "context" |
| 5 | 5 | "database/sql" |
| 6 | 6 | "net/http" |
| 7 | + "strings" | |
| 7 | 8 | "time" |
| 8 | 9 | |
| 9 | 10 | "golang.org/x/sync/errgroup" |
| @@ -23,9 +24,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | ||
| 23 | 24 | subCount int |
| 24 | 25 | userLangs []string |
| 25 | 26 | articles []*db.Article |
| 26 | - articleRecs []*cluster.ArticleRecommendation | |
| 27 | - peopleRecs []*cluster.PersonRecommendation | |
| 28 | - feedRecs []*cluster.FeedRecommendation | |
| 29 | 27 | personalTrending []*db.TrendingItem |
| 30 | 28 | globalTrending []*db.TrendingItem |
| 31 | 29 | ) |
| @@ -64,18 +62,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | ||
| 64 | 62 | return nil |
| 65 | 63 | }) |
| 66 | 64 | |
| 67 | - g.Go(func() error { | |
| 68 | - var err error | |
| 69 | - peopleRecs, err = s.engine.GetPeopleRecommendations(gCtx, user.DID, 6) | |
| 70 | - return err | |
| 71 | - }) | |
| 72 | - | |
| 73 | - g.Go(func() error { | |
| 74 | - var err error | |
| 75 | - feedRecs, err = s.engine.GetFeedRecommendations(gCtx, user.DID, 5) | |
| 76 | - return err | |
| 77 | - }) | |
| 78 | - | |
| 79 | 65 | if err := g.Wait(); err != nil { |
| 80 | 66 | s.logger.Warn("dashboard error", "error", err, "did", user.DID) |
| 81 | 67 | } |
| @@ -91,14 +77,39 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | ||
| 91 | 77 | if err != nil { |
| 92 | 78 | s.logger.Warn("failed to get personal trending", "error", err, "did", user.DID) |
| 93 | 79 | } |
| 80 | + } | |
| 94 | 81 | |
| 95 | - articleRecs, err = s.engine.GetArticleRecommendations(ctx, user.DID, userLangs, 5) | |
| 96 | - if err != nil { | |
| 97 | - s.logger.Warn("failed to get article recommendations", "error", err, "did", user.DID) | |
| 98 | - } | |
| 82 | + settings, _ := s.dbs.Users.GetSettings(ctx, user.DID) | |
| 83 | + digestEnabled := settings != nil && settings.DigestEnabled | |
| 84 | + | |
| 85 | + s.render(w, r, "dashboard.html", map[string]any{ | |
| 86 | + "User": user, | |
| 87 | + "SubscriptionCount": subCount, | |
| 88 | + "UnreadCount": unreadCount, | |
| 89 | + "Articles": articles, | |
| 90 | + "PersonalTrending": personalTrending, | |
| 91 | + "GlobalTrending": globalTrending, | |
| 92 | + "Now": time.Now(), | |
| 93 | + "DigestEnabled": digestEnabled, | |
| 94 | + }) | |
| 95 | +} | |
| 96 | + | |
| 97 | +func (s *Server) handleArticleRecommendations(w http.ResponseWriter, r *http.Request) { | |
| 98 | + user := currentUser(r) | |
| 99 | + ctx := r.Context() | |
| 100 | + | |
| 101 | + userLangs, _ := s.dbs.Users.GetLanguages(ctx, user.DID) | |
| 102 | + articleRecs, err := s.engine.GetArticleRecommendations(ctx, user.DID, userLangs, 5) | |
| 103 | + if err != nil { | |
| 104 | + s.logger.Warn("failed to get article recommendations", "error", err, "did", user.DID) | |
| 105 | + w.WriteHeader(http.StatusOK) | |
| 106 | + return | |
| 99 | 107 | } |
| 100 | 108 | |
| 101 | - resolvePeopleHandles(ctx, peopleRecs) | |
| 109 | + if len(articleRecs) == 0 { | |
| 110 | + w.WriteHeader(http.StatusOK) | |
| 111 | + return | |
| 112 | + } | |
| 102 | 113 | |
| 103 | 114 | articleRecArticles := make([]*db.Article, len(articleRecs)) |
| 104 | 115 | for i, rec := range articleRecs { |
| @@ -123,6 +134,44 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | ||
| 123 | 134 | for _, rec := range articleRecs { |
| 124 | 135 | impressions = append(impressions, feedback.Impression{TargetType: "article", TargetID: rec.URL}) |
| 125 | 136 | } |
| 137 | + if len(impressions) > 0 { | |
| 138 | + if err := s.feedback.RecordImpressions(ctx, user.DID, impressions); err != nil { | |
| 139 | + s.logger.Warn("failed to record impressions", "error", err) | |
| 140 | + } | |
| 141 | + } | |
| 142 | + | |
| 143 | + if cookie, err := r.Cookie("glean_csrf"); err == nil { | |
| 144 | + data := map[string]any{ | |
| 145 | + "ArticleRecommendations": articleRecArticles, | |
| 146 | + "CSRFToken": cookie.Value, | |
| 147 | + } | |
| 148 | + var buf strings.Builder | |
| 149 | + if err := s.templates.ExecuteTemplate(&buf, "partials/article-recommendations.html", data); err != nil { | |
| 150 | + s.logger.Error("article recommendations template error", "error", err) | |
| 151 | + w.WriteHeader(http.StatusOK) | |
| 152 | + return | |
| 153 | + } | |
| 154 | + w.Header().Set("Content-Type", "text/html") | |
| 155 | + w.Write([]byte(buf.String())) | |
| 156 | + return | |
| 157 | + } | |
| 158 | + | |
| 159 | + w.WriteHeader(http.StatusOK) | |
| 160 | +} | |
| 161 | + | |
| 162 | +func (s *Server) handleFeedRecommendations(w http.ResponseWriter, r *http.Request) { | |
| 163 | + user := currentUser(r) | |
| 164 | + ctx := r.Context() | |
| 165 | + | |
| 166 | + subCount, _ := s.dbs.Articles.GetSubscriptionCount(ctx, user.DID) | |
| 167 | + feedRecs, err := s.engine.GetFeedRecommendations(ctx, user.DID, 5) | |
| 168 | + if err != nil { | |
| 169 | + s.logger.Warn("failed to get feed recommendations", "error", err, "did", user.DID) | |
| 170 | + w.WriteHeader(http.StatusOK) | |
| 171 | + return | |
| 172 | + } | |
| 173 | + | |
| 174 | + var impressions []feedback.Impression | |
| 126 | 175 | for _, rec := range feedRecs { |
| 127 | 176 | impressions = append(impressions, feedback.Impression{TargetType: "feed", TargetID: rec.FeedURL}) |
| 128 | 177 | } |
| @@ -132,6 +181,44 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | ||
| 132 | 181 | } |
| 133 | 182 | } |
| 134 | 183 | |
| 184 | + if cookie, err := r.Cookie("glean_csrf"); err == nil { | |
| 185 | + data := map[string]any{ | |
| 186 | + "FeedRecommendations": feedRecs, | |
| 187 | + "SubscriptionCount": subCount, | |
| 188 | + "CSRFToken": cookie.Value, | |
| 189 | + } | |
| 190 | + var buf strings.Builder | |
| 191 | + if err := s.templates.ExecuteTemplate(&buf, "partials/feed-recommendations.html", data); err != nil { | |
| 192 | + s.logger.Error("feed recommendations template error", "error", err) | |
| 193 | + w.WriteHeader(http.StatusOK) | |
| 194 | + return | |
| 195 | + } | |
| 196 | + w.Header().Set("Content-Type", "text/html") | |
| 197 | + w.Write([]byte(buf.String())) | |
| 198 | + return | |
| 199 | + } | |
| 200 | + | |
| 201 | + w.WriteHeader(http.StatusOK) | |
| 202 | +} | |
| 203 | + | |
| 204 | +func (s *Server) handlePeopleRecommendations(w http.ResponseWriter, r *http.Request) { | |
| 205 | + user := currentUser(r) | |
| 206 | + ctx := r.Context() | |
| 207 | + | |
| 208 | + peopleRecs, err := s.engine.GetPeopleRecommendations(ctx, user.DID, 6) | |
| 209 | + if err != nil { | |
| 210 | + s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID) | |
| 211 | + w.WriteHeader(http.StatusOK) | |
| 212 | + return | |
| 213 | + } | |
| 214 | + | |
| 215 | + if len(peopleRecs) == 0 { | |
| 216 | + w.WriteHeader(http.StatusOK) | |
| 217 | + return | |
| 218 | + } | |
| 219 | + | |
| 220 | + resolvePeopleHandles(ctx, peopleRecs) | |
| 221 | + | |
| 135 | 222 | var followedPeople, discoverPeople []*cluster.PersonRecommendation |
| 136 | 223 | for _, p := range peopleRecs { |
| 137 | 224 | if p.IsFollowed { |
| @@ -141,23 +228,24 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | ||
| 141 | 228 | } |
| 142 | 229 | } |
| 143 | 230 | |
| 144 | - settings, _ := s.dbs.Users.GetSettings(ctx, user.DID) | |
| 145 | - digestEnabled := settings != nil && settings.DigestEnabled | |
| 231 | + if cookie, err := r.Cookie("glean_csrf"); err == nil { | |
| 232 | + data := map[string]any{ | |
| 233 | + "FollowedPeople": followedPeople, | |
| 234 | + "DiscoverPeople": discoverPeople, | |
| 235 | + "CSRFToken": cookie.Value, | |
| 236 | + } | |
| 237 | + var buf strings.Builder | |
| 238 | + if err := s.templates.ExecuteTemplate(&buf, "partials/people-recommendations.html", data); err != nil { | |
| 239 | + s.logger.Error("people recommendations template error", "error", err) | |
| 240 | + w.WriteHeader(http.StatusOK) | |
| 241 | + return | |
| 242 | + } | |
| 243 | + w.Header().Set("Content-Type", "text/html") | |
| 244 | + w.Write([]byte(buf.String())) | |
| 245 | + return | |
| 246 | + } | |
| 146 | 247 | |
| 147 | - s.render(w, r, "dashboard.html", map[string]any{ | |
| 148 | - "User": user, | |
| 149 | - "SubscriptionCount": subCount, | |
| 150 | - "UnreadCount": unreadCount, | |
| 151 | - "Articles": articles, | |
| 152 | - "ArticleRecommendations": articleRecArticles, | |
| 153 | - "FeedRecommendations": feedRecs, | |
| 154 | - "FollowedPeople": followedPeople, | |
| 155 | - "DiscoverPeople": discoverPeople, | |
| 156 | - "PersonalTrending": personalTrending, | |
| 157 | - "GlobalTrending": globalTrending, | |
| 158 | - "Now": time.Now(), | |
| 159 | - "DigestEnabled": digestEnabled, | |
| 160 | - }) | |
| 248 | + w.WriteHeader(http.StatusOK) | |
| 161 | 249 | } |
| 162 | 250 | |
| 163 | 251 | func resolvePeopleHandles(ctx context.Context, people []*cluster.PersonRecommendation) { |
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | "context" | 4 | "context" |
| 5 | "database/sql" | 5 | "database/sql" |
| 6 | "net/http" | 6 | "net/http" |
| 7 | + "strings" | ||
| 7 | "time" | 8 | "time" |
| 8 | 9 | ||
| 9 | "golang.org/x/sync/errgroup" | 10 | "golang.org/x/sync/errgroup" |
| @@ -23,9 +24,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | |||
| 23 | subCount int | 24 | subCount int |
| 24 | userLangs []string | 25 | userLangs []string |
| 25 | articles []*db.Article | 26 | articles []*db.Article |
| 26 | - articleRecs []*cluster.ArticleRecommendation | ||
| 27 | - peopleRecs []*cluster.PersonRecommendation | ||
| 28 | - feedRecs []*cluster.FeedRecommendation | ||
| 29 | personalTrending []*db.TrendingItem | 27 | personalTrending []*db.TrendingItem |
| 30 | globalTrending []*db.TrendingItem | 28 | globalTrending []*db.TrendingItem |
| 31 | ) | 29 | ) |
| @@ -64,18 +62,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | |||
| 64 | return nil | 62 | return nil |
| 65 | }) | 63 | }) |
| 66 | 64 | ||
| 67 | - g.Go(func() error { | ||
| 68 | - var err error | ||
| 69 | - peopleRecs, err = s.engine.GetPeopleRecommendations(gCtx, user.DID, 6) | ||
| 70 | - return err | ||
| 71 | - }) | ||
| 72 | - | ||
| 73 | - g.Go(func() error { | ||
| 74 | - var err error | ||
| 75 | - feedRecs, err = s.engine.GetFeedRecommendations(gCtx, user.DID, 5) | ||
| 76 | - return err | ||
| 77 | - }) | ||
| 78 | - | ||
| 79 | if err := g.Wait(); err != nil { | 65 | if err := g.Wait(); err != nil { |
| 80 | s.logger.Warn("dashboard error", "error", err, "did", user.DID) | 66 | s.logger.Warn("dashboard error", "error", err, "did", user.DID) |
| 81 | } | 67 | } |
| @@ -91,14 +77,39 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | |||
| 91 | if err != nil { | 77 | if err != nil { |
| 92 | s.logger.Warn("failed to get personal trending", "error", err, "did", user.DID) | 78 | s.logger.Warn("failed to get personal trending", "error", err, "did", user.DID) |
| 93 | } | 79 | } |
| 80 | + } | ||
| 94 | 81 | ||
| 95 | - articleRecs, err = s.engine.GetArticleRecommendations(ctx, user.DID, userLangs, 5) | 82 | + settings, _ := s.dbs.Users.GetSettings(ctx, user.DID) |
| 96 | - if err != nil { | 83 | + digestEnabled := settings != nil && settings.DigestEnabled |
| 97 | - s.logger.Warn("failed to get article recommendations", "error", err, "did", user.DID) | 84 | + |
| 98 | - } | 85 | + s.render(w, r, "dashboard.html", map[string]any{ |
| 86 | + "User": user, | ||
| 87 | + "SubscriptionCount": subCount, | ||
| 88 | + "UnreadCount": unreadCount, | ||
| 89 | + "Articles": articles, | ||
| 90 | + "PersonalTrending": personalTrending, | ||
| 91 | + "GlobalTrending": globalTrending, | ||
| 92 | + "Now": time.Now(), | ||
| 93 | + "DigestEnabled": digestEnabled, | ||
| 94 | + }) | ||
| 95 | +} | ||
| 96 | + | ||
| 97 | +func (s *Server) handleArticleRecommendations(w http.ResponseWriter, r *http.Request) { | ||
| 98 | + user := currentUser(r) | ||
| 99 | + ctx := r.Context() | ||
| 100 | + | ||
| 101 | + userLangs, _ := s.dbs.Users.GetLanguages(ctx, user.DID) | ||
| 102 | + articleRecs, err := s.engine.GetArticleRecommendations(ctx, user.DID, userLangs, 5) | ||
| 103 | + if err != nil { | ||
| 104 | + s.logger.Warn("failed to get article recommendations", "error", err, "did", user.DID) | ||
| 105 | + w.WriteHeader(http.StatusOK) | ||
| 106 | + return | ||
| 99 | } | 107 | } |
| 100 | 108 | ||
| 101 | - resolvePeopleHandles(ctx, peopleRecs) | 109 | + if len(articleRecs) == 0 { |
| 110 | + w.WriteHeader(http.StatusOK) | ||
| 111 | + return | ||
| 112 | + } | ||
| 102 | 113 | ||
| 103 | articleRecArticles := make([]*db.Article, len(articleRecs)) | 114 | articleRecArticles := make([]*db.Article, len(articleRecs)) |
| 104 | for i, rec := range articleRecs { | 115 | for i, rec := range articleRecs { |
| @@ -123,6 +134,44 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | |||
| 123 | for _, rec := range articleRecs { | 134 | for _, rec := range articleRecs { |
| 124 | impressions = append(impressions, feedback.Impression{TargetType: "article", TargetID: rec.URL}) | 135 | impressions = append(impressions, feedback.Impression{TargetType: "article", TargetID: rec.URL}) |
| 125 | } | 136 | } |
| 137 | + if len(impressions) > 0 { | ||
| 138 | + if err := s.feedback.RecordImpressions(ctx, user.DID, impressions); err != nil { | ||
| 139 | + s.logger.Warn("failed to record impressions", "error", err) | ||
| 140 | + } | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + if cookie, err := r.Cookie("glean_csrf"); err == nil { | ||
| 144 | + data := map[string]any{ | ||
| 145 | + "ArticleRecommendations": articleRecArticles, | ||
| 146 | + "CSRFToken": cookie.Value, | ||
| 147 | + } | ||
| 148 | + var buf strings.Builder | ||
| 149 | + if err := s.templates.ExecuteTemplate(&buf, "partials/article-recommendations.html", data); err != nil { | ||
| 150 | + s.logger.Error("article recommendations template error", "error", err) | ||
| 151 | + w.WriteHeader(http.StatusOK) | ||
| 152 | + return | ||
| 153 | + } | ||
| 154 | + w.Header().Set("Content-Type", "text/html") | ||
| 155 | + w.Write([]byte(buf.String())) | ||
| 156 | + return | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + w.WriteHeader(http.StatusOK) | ||
| 160 | +} | ||
| 161 | + | ||
| 162 | +func (s *Server) handleFeedRecommendations(w http.ResponseWriter, r *http.Request) { | ||
| 163 | + user := currentUser(r) | ||
| 164 | + ctx := r.Context() | ||
| 165 | + | ||
| 166 | + subCount, _ := s.dbs.Articles.GetSubscriptionCount(ctx, user.DID) | ||
| 167 | + feedRecs, err := s.engine.GetFeedRecommendations(ctx, user.DID, 5) | ||
| 168 | + if err != nil { | ||
| 169 | + s.logger.Warn("failed to get feed recommendations", "error", err, "did", user.DID) | ||
| 170 | + w.WriteHeader(http.StatusOK) | ||
| 171 | + return | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + var impressions []feedback.Impression | ||
| 126 | for _, rec := range feedRecs { | 175 | for _, rec := range feedRecs { |
| 127 | impressions = append(impressions, feedback.Impression{TargetType: "feed", TargetID: rec.FeedURL}) | 176 | impressions = append(impressions, feedback.Impression{TargetType: "feed", TargetID: rec.FeedURL}) |
| 128 | } | 177 | } |
| @@ -132,6 +181,44 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | |||
| 132 | } | 181 | } |
| 133 | } | 182 | } |
| 134 | 183 | ||
| 184 | + if cookie, err := r.Cookie("glean_csrf"); err == nil { | ||
| 185 | + data := map[string]any{ | ||
| 186 | + "FeedRecommendations": feedRecs, | ||
| 187 | + "SubscriptionCount": subCount, | ||
| 188 | + "CSRFToken": cookie.Value, | ||
| 189 | + } | ||
| 190 | + var buf strings.Builder | ||
| 191 | + if err := s.templates.ExecuteTemplate(&buf, "partials/feed-recommendations.html", data); err != nil { | ||
| 192 | + s.logger.Error("feed recommendations template error", "error", err) | ||
| 193 | + w.WriteHeader(http.StatusOK) | ||
| 194 | + return | ||
| 195 | + } | ||
| 196 | + w.Header().Set("Content-Type", "text/html") | ||
| 197 | + w.Write([]byte(buf.String())) | ||
| 198 | + return | ||
| 199 | + } | ||
| 200 | + | ||
| 201 | + w.WriteHeader(http.StatusOK) | ||
| 202 | +} | ||
| 203 | + | ||
| 204 | +func (s *Server) handlePeopleRecommendations(w http.ResponseWriter, r *http.Request) { | ||
| 205 | + user := currentUser(r) | ||
| 206 | + ctx := r.Context() | ||
| 207 | + | ||
| 208 | + peopleRecs, err := s.engine.GetPeopleRecommendations(ctx, user.DID, 6) | ||
| 209 | + if err != nil { | ||
| 210 | + s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID) | ||
| 211 | + w.WriteHeader(http.StatusOK) | ||
| 212 | + return | ||
| 213 | + } | ||
| 214 | + | ||
| 215 | + if len(peopleRecs) == 0 { | ||
| 216 | + w.WriteHeader(http.StatusOK) | ||
| 217 | + return | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + resolvePeopleHandles(ctx, peopleRecs) | ||
| 221 | + | ||
| 135 | var followedPeople, discoverPeople []*cluster.PersonRecommendation | 222 | var followedPeople, discoverPeople []*cluster.PersonRecommendation |
| 136 | for _, p := range peopleRecs { | 223 | for _, p := range peopleRecs { |
| 137 | if p.IsFollowed { | 224 | if p.IsFollowed { |
| @@ -141,23 +228,24 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | |||
| 141 | } | 228 | } |
| 142 | } | 229 | } |
| 143 | 230 | ||
| 144 | - settings, _ := s.dbs.Users.GetSettings(ctx, user.DID) | 231 | + if cookie, err := r.Cookie("glean_csrf"); err == nil { |
| 145 | - digestEnabled := settings != nil && settings.DigestEnabled | 232 | + data := map[string]any{ |
| 233 | + "FollowedPeople": followedPeople, | ||
| 234 | + "DiscoverPeople": discoverPeople, | ||
| 235 | + "CSRFToken": cookie.Value, | ||
| 236 | + } | ||
| 237 | + var buf strings.Builder | ||
| 238 | + if err := s.templates.ExecuteTemplate(&buf, "partials/people-recommendations.html", data); err != nil { | ||
| 239 | + s.logger.Error("people recommendations template error", "error", err) | ||
| 240 | + w.WriteHeader(http.StatusOK) | ||
| 241 | + return | ||
| 242 | + } | ||
| 243 | + w.Header().Set("Content-Type", "text/html") | ||
| 244 | + w.Write([]byte(buf.String())) | ||
| 245 | + return | ||
| 246 | + } | ||
| 146 | 247 | ||
| 147 | - s.render(w, r, "dashboard.html", map[string]any{ | 248 | + w.WriteHeader(http.StatusOK) |
| 148 | - "User": user, | ||
| 149 | - "SubscriptionCount": subCount, | ||
| 150 | - "UnreadCount": unreadCount, | ||
| 151 | - "Articles": articles, | ||
| 152 | - "ArticleRecommendations": articleRecArticles, | ||
| 153 | - "FeedRecommendations": feedRecs, | ||
| 154 | - "FollowedPeople": followedPeople, | ||
| 155 | - "DiscoverPeople": discoverPeople, | ||
| 156 | - "PersonalTrending": personalTrending, | ||
| 157 | - "GlobalTrending": globalTrending, | ||
| 158 | - "Now": time.Now(), | ||
| 159 | - "DigestEnabled": digestEnabled, | ||
| 160 | - }) | ||
| 161 | } | 249 | } |
| 162 | 250 | ||
| 163 | func resolvePeopleHandles(ctx context.Context, people []*cluster.PersonRecommendation) { | 251 | func resolvePeopleHandles(ctx context.Context, people []*cluster.PersonRecommendation) { |
modified
internal/server/server.go +3 -0 | @@ -168,6 +168,9 @@ func (s *Server) setupRoutes() { | ||
| 168 | 168 | s.router.Route("/dashboard", func(r chi.Router) { |
| 169 | 169 | r.Use(s.requireAuth) |
| 170 | 170 | r.Get("/", s.handleDashboard) |
| 171 | + r.Get("/article-recommendations", s.handleArticleRecommendations) | |
| 172 | + r.Get("/feed-recommendations", s.handleFeedRecommendations) | |
| 173 | + r.Get("/people-recommendations", s.handlePeopleRecommendations) | |
| 171 | 174 | }) |
| 172 | 175 | |
| 173 | 176 | s.router.Route("/feeds", func(r chi.Router) { |
| @@ -168,6 +168,9 @@ func (s *Server) setupRoutes() { | |||
| 168 | s.router.Route("/dashboard", func(r chi.Router) { | 168 | s.router.Route("/dashboard", func(r chi.Router) { |
| 169 | r.Use(s.requireAuth) | 169 | r.Use(s.requireAuth) |
| 170 | r.Get("/", s.handleDashboard) | 170 | r.Get("/", s.handleDashboard) |
| 171 | + r.Get("/article-recommendations", s.handleArticleRecommendations) | ||
| 172 | + r.Get("/feed-recommendations", s.handleFeedRecommendations) | ||
| 173 | + r.Get("/people-recommendations", s.handlePeopleRecommendations) | ||
| 171 | }) | 174 | }) |
| 172 | 175 | ||
| 173 | s.router.Route("/feeds", func(r chi.Router) { | 176 | s.router.Route("/feeds", func(r chi.Router) { |
modified
internal/tmpl/dashboard.html +105 -48 | @@ -27,13 +27,31 @@ | ||
| 27 | 27 | </a> |
| 28 | 28 | </div> |
| 29 | 29 | {{else if .Articles}} |
| 30 | -{{if .ArticleRecommendations}} | |
| 31 | -<div class="mb-8"> | |
| 32 | - <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2> | |
| 33 | - <div class="space-y-3"> | |
| 34 | - {{range .ArticleRecommendations}} | |
| 35 | - {{template "article-card.html" .}} | |
| 36 | - {{end}} | |
| 30 | +{{if gt .SubscriptionCount 0}} | |
| 31 | +<div id="article-recommendations" hx-get="/dashboard/article-recommendations" hx-trigger="load" hx-swap="innerHTML" hx-indicator="#article-recommendations"> | |
| 32 | + <div class="mb-8"> | |
| 33 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2> | |
| 34 | + <div class="animate-pulse space-y-3"> | |
| 35 | + <div class="bg-spot-surface rounded-xl px-5 py-4"> | |
| 36 | + <div class="flex items-start gap-2.5"> | |
| 37 | + <div class="w-4 h-4 bg-spot-hover rounded shrink-0 mt-0.5"></div> | |
| 38 | + <div class="flex-1 space-y-2"> | |
| 39 | + <div class="h-4 bg-spot-hover rounded w-2/3"></div> | |
| 40 | + <div class="h-3 bg-spot-hover rounded w-full"></div> | |
| 41 | + <div class="h-3 bg-spot-hover rounded w-4/5"></div> | |
| 42 | + </div> | |
| 43 | + </div> | |
| 44 | + </div> | |
| 45 | + <div class="bg-spot-surface rounded-xl px-5 py-4"> | |
| 46 | + <div class="flex items-start gap-2.5"> | |
| 47 | + <div class="w-4 h-4 bg-spot-hover rounded shrink-0 mt-0.5"></div> | |
| 48 | + <div class="flex-1 space-y-2"> | |
| 49 | + <div class="h-4 bg-spot-hover rounded w-1/2"></div> | |
| 50 | + <div class="h-3 bg-spot-hover rounded w-full"></div> | |
| 51 | + </div> | |
| 52 | + </div> | |
| 53 | + </div> | |
| 54 | + </div> | |
| 37 | 55 | </div> |
| 38 | 56 | </div> |
| 39 | 57 | {{end}} |
| @@ -71,13 +89,22 @@ | ||
| 71 | 89 | {{end}} |
| 72 | 90 | </div> |
| 73 | 91 | {{else}} |
| 74 | -{{if .ArticleRecommendations}} | |
| 75 | -<div class="mb-8"> | |
| 76 | - <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2> | |
| 77 | - <div class="space-y-3"> | |
| 78 | - {{range .ArticleRecommendations}} | |
| 79 | - {{template "article-card.html" .}} | |
| 80 | - {{end}} | |
| 92 | +{{if gt .SubscriptionCount 0}} | |
| 93 | +<div id="article-recommendations" hx-get="/dashboard/article-recommendations" hx-trigger="load" hx-swap="innerHTML" hx-indicator="#article-recommendations"> | |
| 94 | + <div class="mb-8"> | |
| 95 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2> | |
| 96 | + <div class="animate-pulse space-y-3"> | |
| 97 | + <div class="bg-spot-surface rounded-xl px-5 py-4"> | |
| 98 | + <div class="flex items-start gap-2.5"> | |
| 99 | + <div class="w-4 h-4 bg-spot-hover rounded shrink-0 mt-0.5"></div> | |
| 100 | + <div class="flex-1 space-y-2"> | |
| 101 | + <div class="h-4 bg-spot-hover rounded w-2/3"></div> | |
| 102 | + <div class="h-3 bg-spot-hover rounded w-full"></div> | |
| 103 | + <div class="h-3 bg-spot-hover rounded w-4/5"></div> | |
| 104 | + </div> | |
| 105 | + </div> | |
| 106 | + </div> | |
| 107 | + </div> | |
| 81 | 108 | </div> |
| 82 | 109 | </div> |
| 83 | 110 | {{end}} |
| @@ -107,46 +134,76 @@ | ||
| 107 | 134 | </div> |
| 108 | 135 | {{end}} |
| 109 | 136 | |
| 110 | -{{if or .FollowedPeople .DiscoverPeople}} | |
| 111 | -<div class="mb-8"> | |
| 112 | - <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| 113 | - {{if .FollowedPeople}} | |
| 114 | - <div> | |
| 115 | - <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2> | |
| 116 | - <div class="space-y-3"> | |
| 117 | - {{range .FollowedPeople}} | |
| 118 | - {{template "profile-card.html" .}} | |
| 119 | - {{end}} | |
| 137 | +<div id="people-recommendations" hx-get="/dashboard/people-recommendations" hx-trigger="load" hx-swap="innerHTML" hx-indicator="#people-recommendations"> | |
| 138 | + <div class="mb-8"> | |
| 139 | + <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| 140 | + <div> | |
| 141 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2> | |
| 142 | + <div class="space-y-3"> | |
| 143 | + <div class="animate-pulse bg-spot-surface rounded-xl p-4 flex items-center gap-3.5"> | |
| 144 | + <div class="w-10 h-10 bg-spot-hover rounded-full"></div> | |
| 145 | + <div class="flex-1 space-y-2"> | |
| 146 | + <div class="h-4 bg-spot-hover rounded w-1/3"></div> | |
| 147 | + <div class="h-3 bg-spot-hover rounded w-1/2"></div> | |
| 148 | + </div> | |
| 149 | + </div> | |
| 150 | + <div class="animate-pulse bg-spot-surface rounded-xl p-4 flex items-center gap-3.5"> | |
| 151 | + <div class="w-10 h-10 bg-spot-hover rounded-full"></div> | |
| 152 | + <div class="flex-1 space-y-2"> | |
| 153 | + <div class="h-4 bg-spot-hover rounded w-1/4"></div> | |
| 154 | + <div class="h-3 bg-spot-hover rounded w-2/5"></div> | |
| 155 | + </div> | |
| 156 | + </div> | |
| 157 | + </div> | |
| 120 | 158 | </div> |
| 121 | - </div> | |
| 122 | - {{end}} | |
| 123 | - {{if .DiscoverPeople}} | |
| 124 | - <div> | |
| 125 | - <h2 class="text-lg font-semibold text-spot-text mb-4">Discover new readers</h2> | |
| 126 | - <div class="space-y-3"> | |
| 127 | - {{range .DiscoverPeople}} | |
| 128 | - {{template "profile-card.html" .}} | |
| 129 | - {{end}} | |
| 159 | + <div> | |
| 160 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Discover new readers</h2> | |
| 161 | + <div class="space-y-3"> | |
| 162 | + <div class="animate-pulse bg-spot-surface rounded-xl p-4 flex items-center gap-3.5"> | |
| 163 | + <div class="w-10 h-10 bg-spot-hover rounded-full"></div> | |
| 164 | + <div class="flex-1 space-y-2"> | |
| 165 | + <div class="h-4 bg-spot-hover rounded w-1/3"></div> | |
| 166 | + <div class="h-3 bg-spot-hover rounded w-1/2"></div> | |
| 167 | + </div> | |
| 168 | + </div> | |
| 169 | + </div> | |
| 130 | 170 | </div> |
| 131 | 171 | </div> |
| 132 | - {{end}} | |
| 133 | 172 | </div> |
| 134 | 173 | </div> |
| 135 | -{{end}} | |
| 136 | 174 | |
| 137 | -{{if .FeedRecommendations}} | |
| 138 | -<div class="mb-8"> | |
| 139 | - <h2 class="text-lg font-semibold text-spot-text mb-4">{{if eq .SubscriptionCount 0}}Popular feeds to get started{{else}}Recommended feeds{{end}}</h2> | |
| 140 | - <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> | |
| 141 | - {{range .FeedRecommendations}} | |
| 142 | - {{template "recommendation-feed-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | |
| 143 | - {{end}} | |
| 175 | +<div id="feed-recommendations" hx-get="/dashboard/feed-recommendations" hx-trigger="load" hx-swap="innerHTML" hx-indicator="#feed-recommendations"> | |
| 176 | + <div class="mb-8"> | |
| 177 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2> | |
| 178 | + <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> | |
| 179 | + <div class="animate-pulse bg-spot-surface rounded-xl p-3.5"> | |
| 180 | + <div class="flex items-center gap-2.5"> | |
| 181 | + <div class="w-5 h-5 bg-spot-hover rounded"></div> | |
| 182 | + <div class="flex-1 space-y-2"> | |
| 183 | + <div class="h-4 bg-spot-hover rounded w-2/3"></div> | |
| 184 | + <div class="h-3 bg-spot-hover rounded w-full"></div> | |
| 185 | + </div> | |
| 186 | + </div> | |
| 187 | + </div> | |
| 188 | + <div class="animate-pulse bg-spot-surface rounded-xl p-3.5"> | |
| 189 | + <div class="flex items-center gap-2.5"> | |
| 190 | + <div class="w-5 h-5 bg-spot-hover rounded"></div> | |
| 191 | + <div class="flex-1 space-y-2"> | |
| 192 | + <div class="h-4 bg-spot-hover rounded w-1/2"></div> | |
| 193 | + <div class="h-3 bg-spot-hover rounded w-4/5"></div> | |
| 194 | + </div> | |
| 195 | + </div> | |
| 196 | + </div> | |
| 197 | + <div class="animate-pulse bg-spot-surface rounded-xl p-3.5"> | |
| 198 | + <div class="flex items-center gap-2.5"> | |
| 199 | + <div class="w-5 h-5 bg-spot-hover rounded"></div> | |
| 200 | + <div class="flex-1 space-y-2"> | |
| 201 | + <div class="h-4 bg-spot-hover rounded w-3/5"></div> | |
| 202 | + <div class="h-3 bg-spot-hover rounded w-full"></div> | |
| 203 | + </div> | |
| 204 | + </div> | |
| 205 | + </div> | |
| 206 | + </div> | |
| 144 | 207 | </div> |
| 145 | 208 | </div> |
| 146 | -{{else}} | |
| 147 | -<div class="mb-8"> | |
| 148 | - <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2> | |
| 149 | - {{template "empty-state.html" (dict "icon" "icon-rss" "title" "No feed suggestions yet" "subtitle" "Subscribe to a few feeds and we'll suggest more based on your reading.")}} | |
| 150 | -</div> | |
| 151 | -{{end}} | |
| 152 | 209 | {{end}} |
| @@ -27,13 +27,31 @@ | |||
| 27 | </a> | 27 | </a> |
| 28 | </div> | 28 | </div> |
| 29 | {{else if .Articles}} | 29 | {{else if .Articles}} |
| 30 | -{{if .ArticleRecommendations}} | 30 | +{{if gt .SubscriptionCount 0}} |
| 31 | -<div class="mb-8"> | 31 | +<div id="article-recommendations" hx-get="/dashboard/article-recommendations" hx-trigger="load" hx-swap="innerHTML" hx-indicator="#article-recommendations"> |
| 32 | - <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2> | 32 | + <div class="mb-8"> |
| 33 | - <div class="space-y-3"> | 33 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2> |
| 34 | - {{range .ArticleRecommendations}} | 34 | + <div class="animate-pulse space-y-3"> |
| 35 | - {{template "article-card.html" .}} | 35 | + <div class="bg-spot-surface rounded-xl px-5 py-4"> |
| 36 | - {{end}} | 36 | + <div class="flex items-start gap-2.5"> |
| 37 | + <div class="w-4 h-4 bg-spot-hover rounded shrink-0 mt-0.5"></div> | ||
| 38 | + <div class="flex-1 space-y-2"> | ||
| 39 | + <div class="h-4 bg-spot-hover rounded w-2/3"></div> | ||
| 40 | + <div class="h-3 bg-spot-hover rounded w-full"></div> | ||
| 41 | + <div class="h-3 bg-spot-hover rounded w-4/5"></div> | ||
| 42 | + </div> | ||
| 43 | + </div> | ||
| 44 | + </div> | ||
| 45 | + <div class="bg-spot-surface rounded-xl px-5 py-4"> | ||
| 46 | + <div class="flex items-start gap-2.5"> | ||
| 47 | + <div class="w-4 h-4 bg-spot-hover rounded shrink-0 mt-0.5"></div> | ||
| 48 | + <div class="flex-1 space-y-2"> | ||
| 49 | + <div class="h-4 bg-spot-hover rounded w-1/2"></div> | ||
| 50 | + <div class="h-3 bg-spot-hover rounded w-full"></div> | ||
| 51 | + </div> | ||
| 52 | + </div> | ||
| 53 | + </div> | ||
| 54 | + </div> | ||
| 37 | </div> | 55 | </div> |
| 38 | </div> | 56 | </div> |
| 39 | {{end}} | 57 | {{end}} |
| @@ -71,13 +89,22 @@ | |||
| 71 | {{end}} | 89 | {{end}} |
| 72 | </div> | 90 | </div> |
| 73 | {{else}} | 91 | {{else}} |
| 74 | -{{if .ArticleRecommendations}} | 92 | +{{if gt .SubscriptionCount 0}} |
| 75 | -<div class="mb-8"> | 93 | +<div id="article-recommendations" hx-get="/dashboard/article-recommendations" hx-trigger="load" hx-swap="innerHTML" hx-indicator="#article-recommendations"> |
| 76 | - <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2> | 94 | + <div class="mb-8"> |
| 77 | - <div class="space-y-3"> | 95 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2> |
| 78 | - {{range .ArticleRecommendations}} | 96 | + <div class="animate-pulse space-y-3"> |
| 79 | - {{template "article-card.html" .}} | 97 | + <div class="bg-spot-surface rounded-xl px-5 py-4"> |
| 80 | - {{end}} | 98 | + <div class="flex items-start gap-2.5"> |
| 99 | + <div class="w-4 h-4 bg-spot-hover rounded shrink-0 mt-0.5"></div> | ||
| 100 | + <div class="flex-1 space-y-2"> | ||
| 101 | + <div class="h-4 bg-spot-hover rounded w-2/3"></div> | ||
| 102 | + <div class="h-3 bg-spot-hover rounded w-full"></div> | ||
| 103 | + <div class="h-3 bg-spot-hover rounded w-4/5"></div> | ||
| 104 | + </div> | ||
| 105 | + </div> | ||
| 106 | + </div> | ||
| 107 | + </div> | ||
| 81 | </div> | 108 | </div> |
| 82 | </div> | 109 | </div> |
| 83 | {{end}} | 110 | {{end}} |
| @@ -107,46 +134,76 @@ | |||
| 107 | </div> | 134 | </div> |
| 108 | {{end}} | 135 | {{end}} |
| 109 | 136 | ||
| 110 | -{{if or .FollowedPeople .DiscoverPeople}} | 137 | +<div id="people-recommendations" hx-get="/dashboard/people-recommendations" hx-trigger="load" hx-swap="innerHTML" hx-indicator="#people-recommendations"> |
| 111 | -<div class="mb-8"> | 138 | + <div class="mb-8"> |
| 112 | - <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | 139 | + <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
| 113 | - {{if .FollowedPeople}} | 140 | + <div> |
| 114 | - <div> | 141 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2> |
| 115 | - <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2> | 142 | + <div class="space-y-3"> |
| 116 | - <div class="space-y-3"> | 143 | + <div class="animate-pulse bg-spot-surface rounded-xl p-4 flex items-center gap-3.5"> |
| 117 | - {{range .FollowedPeople}} | 144 | + <div class="w-10 h-10 bg-spot-hover rounded-full"></div> |
| 118 | - {{template "profile-card.html" .}} | 145 | + <div class="flex-1 space-y-2"> |
| 119 | - {{end}} | 146 | + <div class="h-4 bg-spot-hover rounded w-1/3"></div> |
| 147 | + <div class="h-3 bg-spot-hover rounded w-1/2"></div> | ||
| 148 | + </div> | ||
| 149 | + </div> | ||
| 150 | + <div class="animate-pulse bg-spot-surface rounded-xl p-4 flex items-center gap-3.5"> | ||
| 151 | + <div class="w-10 h-10 bg-spot-hover rounded-full"></div> | ||
| 152 | + <div class="flex-1 space-y-2"> | ||
| 153 | + <div class="h-4 bg-spot-hover rounded w-1/4"></div> | ||
| 154 | + <div class="h-3 bg-spot-hover rounded w-2/5"></div> | ||
| 155 | + </div> | ||
| 156 | + </div> | ||
| 157 | + </div> | ||
| 120 | </div> | 158 | </div> |
| 121 | - </div> | 159 | + <div> |
| 122 | - {{end}} | 160 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Discover new readers</h2> |
| 123 | - {{if .DiscoverPeople}} | 161 | + <div class="space-y-3"> |
| 124 | - <div> | 162 | + <div class="animate-pulse bg-spot-surface rounded-xl p-4 flex items-center gap-3.5"> |
| 125 | - <h2 class="text-lg font-semibold text-spot-text mb-4">Discover new readers</h2> | 163 | + <div class="w-10 h-10 bg-spot-hover rounded-full"></div> |
| 126 | - <div class="space-y-3"> | 164 | + <div class="flex-1 space-y-2"> |
| 127 | - {{range .DiscoverPeople}} | 165 | + <div class="h-4 bg-spot-hover rounded w-1/3"></div> |
| 128 | - {{template "profile-card.html" .}} | 166 | + <div class="h-3 bg-spot-hover rounded w-1/2"></div> |
| 129 | - {{end}} | 167 | + </div> |
| 168 | + </div> | ||
| 169 | + </div> | ||
| 130 | </div> | 170 | </div> |
| 131 | </div> | 171 | </div> |
| 132 | - {{end}} | ||
| 133 | </div> | 172 | </div> |
| 134 | </div> | 173 | </div> |
| 135 | -{{end}} | ||
| 136 | 174 | ||
| 137 | -{{if .FeedRecommendations}} | 175 | +<div id="feed-recommendations" hx-get="/dashboard/feed-recommendations" hx-trigger="load" hx-swap="innerHTML" hx-indicator="#feed-recommendations"> |
| 138 | -<div class="mb-8"> | 176 | + <div class="mb-8"> |
| 139 | - <h2 class="text-lg font-semibold text-spot-text mb-4">{{if eq .SubscriptionCount 0}}Popular feeds to get started{{else}}Recommended feeds{{end}}</h2> | 177 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2> |
| 140 | - <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> | 178 | + <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> |
| 141 | - {{range .FeedRecommendations}} | 179 | + <div class="animate-pulse bg-spot-surface rounded-xl p-3.5"> |
| 142 | - {{template "recommendation-feed-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | 180 | + <div class="flex items-center gap-2.5"> |
| 143 | - {{end}} | 181 | + <div class="w-5 h-5 bg-spot-hover rounded"></div> |
| 182 | + <div class="flex-1 space-y-2"> | ||
| 183 | + <div class="h-4 bg-spot-hover rounded w-2/3"></div> | ||
| 184 | + <div class="h-3 bg-spot-hover rounded w-full"></div> | ||
| 185 | + </div> | ||
| 186 | + </div> | ||
| 187 | + </div> | ||
| 188 | + <div class="animate-pulse bg-spot-surface rounded-xl p-3.5"> | ||
| 189 | + <div class="flex items-center gap-2.5"> | ||
| 190 | + <div class="w-5 h-5 bg-spot-hover rounded"></div> | ||
| 191 | + <div class="flex-1 space-y-2"> | ||
| 192 | + <div class="h-4 bg-spot-hover rounded w-1/2"></div> | ||
| 193 | + <div class="h-3 bg-spot-hover rounded w-4/5"></div> | ||
| 194 | + </div> | ||
| 195 | + </div> | ||
| 196 | + </div> | ||
| 197 | + <div class="animate-pulse bg-spot-surface rounded-xl p-3.5"> | ||
| 198 | + <div class="flex items-center gap-2.5"> | ||
| 199 | + <div class="w-5 h-5 bg-spot-hover rounded"></div> | ||
| 200 | + <div class="flex-1 space-y-2"> | ||
| 201 | + <div class="h-4 bg-spot-hover rounded w-3/5"></div> | ||
| 202 | + <div class="h-3 bg-spot-hover rounded w-full"></div> | ||
| 203 | + </div> | ||
| 204 | + </div> | ||
| 205 | + </div> | ||
| 206 | + </div> | ||
| 144 | </div> | 207 | </div> |
| 145 | </div> | 208 | </div> |
| 146 | -{{else}} | ||
| 147 | -<div class="mb-8"> | ||
| 148 | - <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2> | ||
| 149 | - {{template "empty-state.html" (dict "icon" "icon-rss" "title" "No feed suggestions yet" "subtitle" "Subscribe to a few feeds and we'll suggest more based on your reading.")}} | ||
| 150 | -</div> | ||
| 151 | -{{end}} | ||
| 152 | {{end}} | 209 | {{end}} |
modified
internal/tmpl/partials/article-card.html +1 -2 | @@ -22,9 +22,8 @@ | ||
| 22 | 22 | <span>{{if .IsRead.Bool}}Unread{{else}}Read{{end}}</span> |
| 23 | 23 | </button> |
| 24 | 24 | {{if .DismissURL}} |
| 25 | - <button hx-post="{{.DismissURL}}" hx-target="closest article" hx-swap="delete" title="Remove from recommendations" | |
| 25 | + <button hx-post="{{.DismissURL}}" hx-target="closest article" hx-swap="delete" hx-vals='{"{{.DismissField}}": "{{.DismissValue}}"}' title="Remove from recommendations" | |
| 26 | 26 | class="group inline-flex items-center justify-center gap-1 text-[10px] text-spot-text hover:text-spot-red hover:bg-spot-red/10 hover:border-spot-red/20 uppercase tracking-button px-2 py-0.5 rounded-pill bg-spot-surface border border-spot-divider transition w-full"> |
| 27 | - <input type="hidden" name="{{.DismissField}}" value="{{.DismissValue}}"> | |
| 28 | 27 | <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg> |
| 29 | 28 | <span>Hide</span> |
| 30 | 29 | </button> |
| @@ -22,9 +22,8 @@ | |||
| 22 | <span>{{if .IsRead.Bool}}Unread{{else}}Read{{end}}</span> | 22 | <span>{{if .IsRead.Bool}}Unread{{else}}Read{{end}}</span> |
| 23 | </button> | 23 | </button> |
| 24 | {{if .DismissURL}} | 24 | {{if .DismissURL}} |
| 25 | - <button hx-post="{{.DismissURL}}" hx-target="closest article" hx-swap="delete" title="Remove from recommendations" | 25 | + <button hx-post="{{.DismissURL}}" hx-target="closest article" hx-swap="delete" hx-vals='{"{{.DismissField}}": "{{.DismissValue}}"}' title="Remove from recommendations" |
| 26 | class="group inline-flex items-center justify-center gap-1 text-[10px] text-spot-text hover:text-spot-red hover:bg-spot-red/10 hover:border-spot-red/20 uppercase tracking-button px-2 py-0.5 rounded-pill bg-spot-surface border border-spot-divider transition w-full"> | 26 | class="group inline-flex items-center justify-center gap-1 text-[10px] text-spot-text hover:text-spot-red hover:bg-spot-red/10 hover:border-spot-red/20 uppercase tracking-button px-2 py-0.5 rounded-pill bg-spot-surface border border-spot-divider transition w-full"> |
| 27 | - <input type="hidden" name="{{.DismissField}}" value="{{.DismissValue}}"> | ||
| 28 | <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg> | 27 | <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg> |
| 29 | <span>Hide</span> | 28 | <span>Hide</span> |
| 30 | </button> | 29 | </button> |
added
internal/tmpl/partials/article-recommendations.html +10 -0 | new file mode 100644 | ||
| @@ -0,0 +1,10 @@ | ||
| 1 | +{{define "partials/article-recommendations.html"}} | |
| 2 | +<div class="mb-8"> | |
| 3 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2> | |
| 4 | + <div class="space-y-3"> | |
| 5 | + {{range .ArticleRecommendations}} | |
| 6 | + {{template "article-card.html" .}} | |
| 7 | + {{end}} | |
| 8 | + </div> | |
| 9 | +</div> | |
| 10 | +{{end}} | |
| new file mode 100644 | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | +{{define "partials/article-recommendations.html"}} | ||
| 2 | +<div class="mb-8"> | ||
| 3 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended for you</h2> | ||
| 4 | + <div class="space-y-3"> | ||
| 5 | + {{range .ArticleRecommendations}} | ||
| 6 | + {{template "article-card.html" .}} | ||
| 7 | + {{end}} | ||
| 8 | + </div> | ||
| 9 | +</div> | ||
| 10 | +{{end}} | ||
added
internal/tmpl/partials/feed-recommendations.html +17 -0 | new file mode 100644 | ||
| @@ -0,0 +1,17 @@ | ||
| 1 | +{{define "partials/feed-recommendations.html"}} | |
| 2 | +{{if .FeedRecommendations}} | |
| 3 | +<div class="mb-8"> | |
| 4 | + <h2 class="text-lg font-semibold text-spot-text mb-4">{{if eq .SubscriptionCount 0}}Popular feeds to get started{{else}}Recommended feeds{{end}}</h2> | |
| 5 | + <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> | |
| 6 | + {{range .FeedRecommendations}} | |
| 7 | + {{template "recommendation-feed-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | |
| 8 | + {{end}} | |
| 9 | + </div> | |
| 10 | +</div> | |
| 11 | +{{else}} | |
| 12 | +<div class="mb-8"> | |
| 13 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2> | |
| 14 | + {{template "empty-state.html" (dict "icon" "icon-rss" "title" "No feed suggestions yet" "subtitle" "Subscribe to a few feeds and we'll suggest more based on your reading.")}} | |
| 15 | +</div> | |
| 16 | +{{end}} | |
| 17 | +{{end}} | |
| new file mode 100644 | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | +{{define "partials/feed-recommendations.html"}} | ||
| 2 | +{{if .FeedRecommendations}} | ||
| 3 | +<div class="mb-8"> | ||
| 4 | + <h2 class="text-lg font-semibold text-spot-text mb-4">{{if eq .SubscriptionCount 0}}Popular feeds to get started{{else}}Recommended feeds{{end}}</h2> | ||
| 5 | + <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> | ||
| 6 | + {{range .FeedRecommendations}} | ||
| 7 | + {{template "recommendation-feed-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | ||
| 8 | + {{end}} | ||
| 9 | + </div> | ||
| 10 | +</div> | ||
| 11 | +{{else}} | ||
| 12 | +<div class="mb-8"> | ||
| 13 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2> | ||
| 14 | + {{template "empty-state.html" (dict "icon" "icon-rss" "title" "No feed suggestions yet" "subtitle" "Subscribe to a few feeds and we'll suggest more based on your reading.")}} | ||
| 15 | +</div> | ||
| 16 | +{{end}} | ||
| 17 | +{{end}} | ||
added
internal/tmpl/partials/people-recommendations.html +28 -0 | new file mode 100644 | ||
| @@ -0,0 +1,28 @@ | ||
| 1 | +{{define "partials/people-recommendations.html"}} | |
| 2 | +{{if or .FollowedPeople .DiscoverPeople}} | |
| 3 | +<div class="mb-8"> | |
| 4 | + <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| 5 | + {{if .FollowedPeople}} | |
| 6 | + <div> | |
| 7 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2> | |
| 8 | + <div class="space-y-3"> | |
| 9 | + {{range .FollowedPeople}} | |
| 10 | + {{template "profile-card.html" .}} | |
| 11 | + {{end}} | |
| 12 | + </div> | |
| 13 | + </div> | |
| 14 | + {{end}} | |
| 15 | + {{if .DiscoverPeople}} | |
| 16 | + <div> | |
| 17 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Discover new readers</h2> | |
| 18 | + <div class="space-y-3"> | |
| 19 | + {{range .DiscoverPeople}} | |
| 20 | + {{template "profile-card.html" .}} | |
| 21 | + {{end}} | |
| 22 | + </div> | |
| 23 | + </div> | |
| 24 | + {{end}} | |
| 25 | + </div> | |
| 26 | +</div> | |
| 27 | +{{end}} | |
| 28 | +{{end}} | |
| new file mode 100644 | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | +{{define "partials/people-recommendations.html"}} | ||
| 2 | +{{if or .FollowedPeople .DiscoverPeople}} | ||
| 3 | +<div class="mb-8"> | ||
| 4 | + <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | ||
| 5 | + {{if .FollowedPeople}} | ||
| 6 | + <div> | ||
| 7 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2> | ||
| 8 | + <div class="space-y-3"> | ||
| 9 | + {{range .FollowedPeople}} | ||
| 10 | + {{template "profile-card.html" .}} | ||
| 11 | + {{end}} | ||
| 12 | + </div> | ||
| 13 | + </div> | ||
| 14 | + {{end}} | ||
| 15 | + {{if .DiscoverPeople}} | ||
| 16 | + <div> | ||
| 17 | + <h2 class="text-lg font-semibold text-spot-text mb-4">Discover new readers</h2> | ||
| 18 | + <div class="space-y-3"> | ||
| 19 | + {{range .DiscoverPeople}} | ||
| 20 | + {{template "profile-card.html" .}} | ||
| 21 | + {{end}} | ||
| 22 | + </div> | ||
| 23 | + </div> | ||
| 24 | + {{end}} | ||
| 25 | + </div> | ||
| 26 | +</div> | ||
| 27 | +{{end}} | ||
| 28 | +{{end}} | ||