nandi/gleanpublic Fork 0
71d0cbb
Commits
Clone
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

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

Extract recommendations into separate endpoints/templates in dashboardUnverified

Julien Robert committed 2026-06-04T09:32:02+02:00 Browse files
71d0cbb parent: ce55b7c
modified internal/server/dashboard_handler.go +124 -36
@@ -4,6 +4,7 @@ import (
44 "context"
55 "database/sql"
66 "net/http"
7+ "strings"
78 "time"
89
910 "golang.org/x/sync/errgroup"
@@ -23,9 +24,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
2324 subCount int
2425 userLangs []string
2526 articles []*db.Article
26- articleRecs []*cluster.ArticleRecommendation
27- peopleRecs []*cluster.PersonRecommendation
28- feedRecs []*cluster.FeedRecommendation
2927 personalTrending []*db.TrendingItem
3028 globalTrending []*db.TrendingItem
3129 )
@@ -64,18 +62,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
6462 return nil
6563 })
6664
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-
7965 if err := g.Wait(); err != nil {
8066 s.logger.Warn("dashboard error", "error", err, "did", user.DID)
8167 }
@@ -91,14 +77,39 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
9177 if err != nil {
9278 s.logger.Warn("failed to get personal trending", "error", err, "did", user.DID)
9379 }
80+ }
9481
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
99107 }
100108
101- resolvePeopleHandles(ctx, peopleRecs)
109+ if len(articleRecs) == 0 {
110+ w.WriteHeader(http.StatusOK)
111+ return
112+ }
102113
103114 articleRecArticles := make([]*db.Article, len(articleRecs))
104115 for i, rec := range articleRecs {
@@ -123,6 +134,44 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
123134 for _, rec := range articleRecs {
124135 impressions = append(impressions, feedback.Impression{TargetType: "article", TargetID: rec.URL})
125136 }
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
126175 for _, rec := range feedRecs {
127176 impressions = append(impressions, feedback.Impression{TargetType: "feed", TargetID: rec.FeedURL})
128177 }
@@ -132,6 +181,44 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
132181 }
133182 }
134183
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+
135222 var followedPeople, discoverPeople []*cluster.PersonRecommendation
136223 for _, p := range peopleRecs {
137224 if p.IsFollowed {
@@ -141,23 +228,24 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
141228 }
142229 }
143230
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+ }
146247
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)
161249 }
162250
163251 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 int24 subCount int
24 userLangs []string25 userLangs []string
25 articles []*db.Article26 articles []*db.Article
26- articleRecs []*cluster.ArticleRecommendation
27- peopleRecs []*cluster.PersonRecommendation
28- feedRecs []*cluster.FeedRecommendation
29 personalTrending []*db.TrendingItem27 personalTrending []*db.TrendingItem
30 globalTrending []*db.TrendingItem28 globalTrending []*db.TrendingItem
31 )29 )
@@ -64,18 +62,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
64 return nil62 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.PersonRecommendation222 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.DigestEnabled232+ 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() {
168168 s.router.Route("/dashboard", func(r chi.Router) {
169169 r.Use(s.requireAuth)
170170 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)
171174 })
172175
173176 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 @@
2727 </a>
2828 </div>
2929 {{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>
3755 </div>
3856 </div>
3957 {{end}}
@@ -71,13 +89,22 @@
7189 {{end}}
7290 </div>
7391 {{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>
81108 </div>
82109 </div>
83110 {{end}}
@@ -107,46 +134,76 @@
107134 </div>
108135 {{end}}
109136
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>
120158 </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>
130170 </div>
131171 </div>
132- {{end}}
133172 </div>
134173 </div>
135-{{end}}
136174
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>
144207 </div>
145208 </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}}
152209 {{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 @@
2222 <span>{{if .IsRead.Bool}}Unread{{else}}Read{{end}}</span>
2323 </button>
2424 {{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"
2626 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}}">
2827 <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>
2928 <span>Hide</span>
3029 </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}}