Improve recommendation cardsUnverified
b6dcf3d parent: f847c6f modified
internal/cluster/scoring.go +12 -10 | @@ -27,15 +27,16 @@ type PersonRecommendation struct { | ||
| 27 | 27 | } |
| 28 | 28 | |
| 29 | 29 | type ArticleRecommendation struct { |
| 30 | - ArticleID int64 | |
| 31 | - Title string | |
| 32 | - URL string | |
| 33 | - FeedURL string | |
| 34 | - FeedTitle string | |
| 35 | - Author string | |
| 36 | - Summary string | |
| 37 | - Published sql.NullTime | |
| 38 | - Score float64 | |
| 30 | + ArticleID int64 | |
| 31 | + Title string | |
| 32 | + URL string | |
| 33 | + FeedURL string | |
| 34 | + FeedTitle string | |
| 35 | + FaviconURL string | |
| 36 | + Author string | |
| 37 | + Summary string | |
| 38 | + Published sql.NullTime | |
| 39 | + Score float64 | |
| 39 | 40 | } |
| 40 | 41 | |
| 41 | 42 | func (e *Engine) GetFeedRecommendations(ctx context.Context, userDID string, limit int) ([]*FeedRecommendation, error) { |
| @@ -215,6 +216,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user | ||
| 215 | 216 | GROUP BY l.feed_url, l.article_url |
| 216 | 217 | ) |
| 217 | 218 | SELECT a.id, a.title, COALESCE(a.url, ''), la.feed_url, COALESCE(f.title, ''), |
| 219 | + COALESCE(f.favicon_url, ''), | |
| 218 | 220 | COALESCE(a.author, ''), COALESCE(a.summary, ''), a.published, |
| 219 | 221 | COALESCE(la.like_signal, 0) * ? |
| 220 | 222 | + COALESCE(sl.social, 0) * ? |
| @@ -237,7 +239,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user | ||
| 237 | 239 | for rows.Next() { |
| 238 | 240 | rec := &ArticleRecommendation{} |
| 239 | 241 | if err := rows.Scan(&rec.ArticleID, &rec.Title, &rec.URL, &rec.FeedURL, &rec.FeedTitle, |
| 240 | - &rec.Author, &rec.Summary, &rec.Published, &rec.Score); err != nil { | |
| 242 | + &rec.FaviconURL, &rec.Author, &rec.Summary, &rec.Published, &rec.Score); err != nil { | |
| 241 | 243 | return nil, err |
| 242 | 244 | } |
| 243 | 245 | recs = append(recs, rec) |
| @@ -27,15 +27,16 @@ type PersonRecommendation struct { | |||
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | type ArticleRecommendation struct { | 29 | type ArticleRecommendation struct { |
| 30 | - ArticleID int64 | 30 | + ArticleID int64 |
| 31 | - Title string | 31 | + Title string |
| 32 | - URL string | 32 | + URL string |
| 33 | - FeedURL string | 33 | + FeedURL string |
| 34 | - FeedTitle string | 34 | + FeedTitle string |
| 35 | - Author string | 35 | + FaviconURL string |
| 36 | - Summary string | 36 | + Author string |
| 37 | - Published sql.NullTime | 37 | + Summary string |
| 38 | - Score float64 | 38 | + Published sql.NullTime |
| 39 | + Score float64 | ||
| 39 | } | 40 | } |
| 40 | 41 | ||
| 41 | func (e *Engine) GetFeedRecommendations(ctx context.Context, userDID string, limit int) ([]*FeedRecommendation, error) { | 42 | func (e *Engine) GetFeedRecommendations(ctx context.Context, userDID string, limit int) ([]*FeedRecommendation, error) { |
| @@ -215,6 +216,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user | |||
| 215 | GROUP BY l.feed_url, l.article_url | 216 | GROUP BY l.feed_url, l.article_url |
| 216 | ) | 217 | ) |
| 217 | SELECT a.id, a.title, COALESCE(a.url, ''), la.feed_url, COALESCE(f.title, ''), | 218 | SELECT a.id, a.title, COALESCE(a.url, ''), la.feed_url, COALESCE(f.title, ''), |
| 219 | + COALESCE(f.favicon_url, ''), | ||
| 218 | COALESCE(a.author, ''), COALESCE(a.summary, ''), a.published, | 220 | COALESCE(a.author, ''), COALESCE(a.summary, ''), a.published, |
| 219 | COALESCE(la.like_signal, 0) * ? | 221 | COALESCE(la.like_signal, 0) * ? |
| 220 | + COALESCE(sl.social, 0) * ? | 222 | + COALESCE(sl.social, 0) * ? |
| @@ -237,7 +239,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user | |||
| 237 | for rows.Next() { | 239 | for rows.Next() { |
| 238 | rec := &ArticleRecommendation{} | 240 | rec := &ArticleRecommendation{} |
| 239 | if err := rows.Scan(&rec.ArticleID, &rec.Title, &rec.URL, &rec.FeedURL, &rec.FeedTitle, | 241 | if err := rows.Scan(&rec.ArticleID, &rec.Title, &rec.URL, &rec.FeedURL, &rec.FeedTitle, |
| 240 | - &rec.Author, &rec.Summary, &rec.Published, &rec.Score); err != nil { | 242 | + &rec.FaviconURL, &rec.Author, &rec.Summary, &rec.Published, &rec.Score); err != nil { |
| 241 | return nil, err | 243 | return nil, err |
| 242 | } | 244 | } |
| 243 | recs = append(recs, rec) | 245 | recs = append(recs, rec) |
modified
internal/tmpl/dashboard.html +3 -11 | @@ -50,7 +50,7 @@ | ||
| 50 | 50 | <h2 class="text-lg font-semibold text-spot-text mb-4">Popular feeds to get started</h2> |
| 51 | 51 | <div class="space-y-3"> |
| 52 | 52 | {{range .FeedRecommendations}} |
| 53 | - {{template "recommendation-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | |
| 53 | + {{template "recommendation-feed-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | |
| 54 | 54 | {{end}} |
| 55 | 55 | </div> |
| 56 | 56 | </div> |
| @@ -68,15 +68,7 @@ | ||
| 68 | 68 | <h2 class="text-lg font-semibold text-spot-text mb-3">Recommended for you</h2> |
| 69 | 69 | <div class="space-y-3"> |
| 70 | 70 | {{range .ArticleRecommendations}} |
| 71 | - <div class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition"> | |
| 72 | - <a href="/articles/{{.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> | |
| 73 | - <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> | |
| 74 | - {{if .Author}}<span>{{.Author}}</span>{{end}} | |
| 75 | - <span class="text-spot-muted"><a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-green transition">{{.FeedTitle}}</a></span> | |
| 76 | - {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2"}}</span>{{end}} | |
| 77 | - </div> | |
| 78 | - {{if .Summary}}<p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{.Summary}}</p>{{end}} | |
| 79 | - </div> | |
| 71 | + {{template "recommendation-article-card.html" .}} | |
| 80 | 72 | {{end}} |
| 81 | 73 | </div> |
| 82 | 74 | </div> |
| @@ -131,7 +123,7 @@ | ||
| 131 | 123 | <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2> |
| 132 | 124 | <div class="space-y-3"> |
| 133 | 125 | {{range .FeedRecommendations}} |
| 134 | - {{template "recommendation-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | |
| 126 | + {{template "recommendation-feed-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | |
| 135 | 127 | {{end}} |
| 136 | 128 | </div> |
| 137 | 129 | </div> |
| @@ -50,7 +50,7 @@ | |||
| 50 | <h2 class="text-lg font-semibold text-spot-text mb-4">Popular feeds to get started</h2> | 50 | <h2 class="text-lg font-semibold text-spot-text mb-4">Popular feeds to get started</h2> |
| 51 | <div class="space-y-3"> | 51 | <div class="space-y-3"> |
| 52 | {{range .FeedRecommendations}} | 52 | {{range .FeedRecommendations}} |
| 53 | - {{template "recommendation-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | 53 | + {{template "recommendation-feed-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} |
| 54 | {{end}} | 54 | {{end}} |
| 55 | </div> | 55 | </div> |
| 56 | </div> | 56 | </div> |
| @@ -68,15 +68,7 @@ | |||
| 68 | <h2 class="text-lg font-semibold text-spot-text mb-3">Recommended for you</h2> | 68 | <h2 class="text-lg font-semibold text-spot-text mb-3">Recommended for you</h2> |
| 69 | <div class="space-y-3"> | 69 | <div class="space-y-3"> |
| 70 | {{range .ArticleRecommendations}} | 70 | {{range .ArticleRecommendations}} |
| 71 | - <div class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition"> | 71 | + {{template "recommendation-article-card.html" .}} |
| 72 | - <a href="/articles/{{.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> | ||
| 73 | - <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> | ||
| 74 | - {{if .Author}}<span>{{.Author}}</span>{{end}} | ||
| 75 | - <span class="text-spot-muted"><a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-green transition">{{.FeedTitle}}</a></span> | ||
| 76 | - {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2"}}</span>{{end}} | ||
| 77 | - </div> | ||
| 78 | - {{if .Summary}}<p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{.Summary}}</p>{{end}} | ||
| 79 | - </div> | ||
| 80 | {{end}} | 72 | {{end}} |
| 81 | </div> | 73 | </div> |
| 82 | </div> | 74 | </div> |
| @@ -131,7 +123,7 @@ | |||
| 131 | <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2> | 123 | <h2 class="text-lg font-semibold text-spot-text mb-4">Recommended feeds</h2> |
| 132 | <div class="space-y-3"> | 124 | <div class="space-y-3"> |
| 133 | {{range .FeedRecommendations}} | 125 | {{range .FeedRecommendations}} |
| 134 | - {{template "recommendation-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | 126 | + {{template "recommendation-feed-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} |
| 135 | {{end}} | 127 | {{end}} |
| 136 | </div> | 128 | </div> |
| 137 | </div> | 129 | </div> |
modified
internal/tmpl/feeds.html +1 -1 | @@ -19,7 +19,7 @@ | ||
| 19 | 19 | <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide mb-3">Recommended feeds</h2> |
| 20 | 20 | <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> |
| 21 | 21 | {{range .FeedRecommendations}} |
| 22 | - {{template "recommendation-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | |
| 22 | + {{template "recommendation-feed-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | |
| 23 | 23 | {{end}} |
| 24 | 24 | </div> |
| 25 | 25 | </div> |
| @@ -19,7 +19,7 @@ | |||
| 19 | <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide mb-3">Recommended feeds</h2> | 19 | <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide mb-3">Recommended feeds</h2> |
| 20 | <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> | 20 | <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> |
| 21 | {{range .FeedRecommendations}} | 21 | {{range .FeedRecommendations}} |
| 22 | - {{template "recommendation-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} | 22 | + {{template "recommendation-feed-card.html" (dict "title" .Title "feed_url" .FeedURL "description" .Description "favicon_url" .FaviconURL "subscriber_count" .SubscriberCount "CSRFToken" $.CSRFToken)}} |
| 23 | {{end}} | 23 | {{end}} |
| 24 | </div> | 24 | </div> |
| 25 | </div> | 25 | </div> |
added
internal/tmpl/partials/recommendation-article-card.html +27 -0 | new file mode 100644 | ||
| @@ -0,0 +1,27 @@ | ||
| 1 | +{{define "recommendation-article-card.html"}} | |
| 2 | +<article data-article-url="{{.URL}}" class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot relative"> | |
| 3 | + <div class="flex items-start justify-between gap-4"> | |
| 4 | + <div class="min-w-0 flex-1"> | |
| 5 | + <div class="flex items-center gap-2"> | |
| 6 | + <span class="w-2 h-2 rounded-full bg-spot-green shrink-0"></span> | |
| 7 | + <a href="/articles/{{.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> | |
| 8 | + </div> | |
| 9 | + <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> | |
| 10 | + {{if .FaviconURL}}{{template "favicon" dict "src" .FaviconURL "size" "w-4 h-4"}}{{end}} | |
| 11 | + {{if .Author}}<span>{{.Author}}</span>{{end}} | |
| 12 | + {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2"}}</span>{{end}} | |
| 13 | + <span class="text-spot-muted"><a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-green transition">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a></span> | |
| 14 | + </div> | |
| 15 | + {{if .Summary}}<p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{plainText .Summary}}</p>{{end}} | |
| 16 | + </div> | |
| 17 | + <div class="shrink-0 pt-1"> | |
| 18 | + <button hx-post="/articles/dismiss" hx-swap="none" hx-include="#dismiss-{{.ArticleID}}" | |
| 19 | + class="text-[10px] text-spot-secondary hover:text-spot-text uppercase tracking-button transition flex items-center gap-1"> | |
| 20 | + <input type="hidden" name="article_url" value="{{.URL}}" id="dismiss-{{.ArticleID}}"> | |
| 21 | + <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> | |
| 22 | + Dismiss | |
| 23 | + </button> | |
| 24 | + </div> | |
| 25 | + </div> | |
| 26 | +</article> | |
| 27 | +{{end}} | |
| \ No newline at end of file | ||
| new file mode 100644 | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | +{{define "recommendation-article-card.html"}} | ||
| 2 | +<article data-article-url="{{.URL}}" class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot relative"> | ||
| 3 | + <div class="flex items-start justify-between gap-4"> | ||
| 4 | + <div class="min-w-0 flex-1"> | ||
| 5 | + <div class="flex items-center gap-2"> | ||
| 6 | + <span class="w-2 h-2 rounded-full bg-spot-green shrink-0"></span> | ||
| 7 | + <a href="/articles/{{.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> | ||
| 8 | + </div> | ||
| 9 | + <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> | ||
| 10 | + {{if .FaviconURL}}{{template "favicon" dict "src" .FaviconURL "size" "w-4 h-4"}}{{end}} | ||
| 11 | + {{if .Author}}<span>{{.Author}}</span>{{end}} | ||
| 12 | + {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2"}}</span>{{end}} | ||
| 13 | + <span class="text-spot-muted"><a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-green transition">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a></span> | ||
| 14 | + </div> | ||
| 15 | + {{if .Summary}}<p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{plainText .Summary}}</p>{{end}} | ||
| 16 | + </div> | ||
| 17 | + <div class="shrink-0 pt-1"> | ||
| 18 | + <button hx-post="/articles/dismiss" hx-swap="none" hx-include="#dismiss-{{.ArticleID}}" | ||
| 19 | + class="text-[10px] text-spot-secondary hover:text-spot-text uppercase tracking-button transition flex items-center gap-1"> | ||
| 20 | + <input type="hidden" name="article_url" value="{{.URL}}" id="dismiss-{{.ArticleID}}"> | ||
| 21 | + <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> | ||
| 22 | + Dismiss | ||
| 23 | + </button> | ||
| 24 | + </div> | ||
| 25 | + </div> | ||
| 26 | +</article> | ||
| 27 | +{{end}} | ||
| \ No newline at end of file | \ No newline at end of file | ||
renamed
internal/tmpl/partials/recommendation-feed-card.html +1 -1 | similarity index 97% | ||
| rename from internal/tmpl/partials/recommendation-card.html | ||
| rename to internal/tmpl/partials/recommendation-feed-card.html | ||
| @@ -1,4 +1,4 @@ | ||
| 1 | -{{define "recommendation-card.html"}} | |
| 1 | +{{define "recommendation-feed-card.html"}} | |
| 2 | 2 | <div class="recommendation-card bg-spot-surface rounded-xl p-3 hover:bg-spot-hover-50 transition"> |
| 3 | 3 | <div class="flex items-center justify-between gap-2"> |
| 4 | 4 | <a href="/articles?feed={{.feed_url}}" class="min-w-0 flex items-center gap-2 flex-1"> |
| similarity index 97% | |||
| rename from internal/tmpl/partials/recommendation-card.html | |||
| rename to internal/tmpl/partials/recommendation-feed-card.html | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | -{{define "recommendation-card.html"}} | 1 | +{{define "recommendation-feed-card.html"}} |
| 2 | <div class="recommendation-card bg-spot-surface rounded-xl p-3 hover:bg-spot-hover-50 transition"> | 2 | <div class="recommendation-card bg-spot-surface rounded-xl p-3 hover:bg-spot-hover-50 transition"> |
| 3 | <div class="flex items-center justify-between gap-2"> | 3 | <div class="flex items-center justify-between gap-2"> |
| 4 | <a href="/articles?feed={{.feed_url}}" class="min-w-0 flex items-center gap-2 flex-1"> | 4 | <a href="/articles?feed={{.feed_url}}" class="min-w-0 flex items-center gap-2 flex-1"> |