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

Improve templates by using more partialsUnverified

Julien Robert committed 2026-04-21T21:54:56+02:00 Browse files
8467321 parent: fa729b0
modified internal/server/feeds_handler.go +14 -11
@@ -114,12 +114,7 @@ func (s *Server) handleAddFeed(w http.ResponseWriter, r *http.Request) {
114114
115115 if err := s.db.CreateSubscription(r.Context(), user.DID, feedURL, feedTitle, category, subURI, subCID); err != nil {
116116 if errors.Is(err, db.ErrDuplicateSubscription) {
117- subs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 100, 0)
118- s.render(w, r, "feed_list.html", map[string]any{
119- "User": user,
120- "Subscriptions": subs,
121- "Error": "Already subscribed to this feed.",
122- })
117+ http.Error(w, "Already subscribed to this feed.", http.StatusConflict)
123118 return
124119 }
125120 s.logger.Error("failed to create subscription", "error", err)
@@ -127,10 +122,18 @@ func (s *Server) handleAddFeed(w http.ResponseWriter, r *http.Request) {
127122 return
128123 }
129124
130- subs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 100, 0)
131- s.render(w, r, "feed_list.html", map[string]any{
132- "User": user,
133- "Subscriptions": subs,
125+ sub, _ := s.db.GetSubscription(r.Context(), user.DID, feedURL)
126+ if sub == nil {
127+ w.WriteHeader(http.StatusNoContent)
128+ return
129+ }
130+ s.render(w, r, "feed-item.html", map[string]any{
131+ "User": user,
132+ "FeedURL": sub.FeedURL,
133+ "FeedTitle": sub.FeedTitle,
134+ "Category": sub.Category,
135+ "FaviconURL": sub.FaviconURL,
136+ "UnreadCount": sub.UnreadCount,
134137 })
135138 }
136139
@@ -313,7 +316,7 @@ func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) {
313316 }
314317
315318 subs, _ = s.db.ListSubscriptions(r.Context(), user.DID, "", 100, 0)
316- s.render(w, r, "feeds.html", map[string]any{
319+ s.render(w, r, "feed-list.html", map[string]any{
317320 "User": user,
318321 "Subscriptions": subs,
319322 })
@@ -114,12 +114,7 @@ func (s *Server) handleAddFeed(w http.ResponseWriter, r *http.Request) {
114 114
115 if err := s.db.CreateSubscription(r.Context(), user.DID, feedURL, feedTitle, category, subURI, subCID); err != nil {115 if err := s.db.CreateSubscription(r.Context(), user.DID, feedURL, feedTitle, category, subURI, subCID); err != nil {
116 if errors.Is(err, db.ErrDuplicateSubscription) {116 if errors.Is(err, db.ErrDuplicateSubscription) {
117- subs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 100, 0)117+ http.Error(w, "Already subscribed to this feed.", http.StatusConflict)
118- s.render(w, r, "feed_list.html", map[string]any{
119- "User": user,
120- "Subscriptions": subs,
121- "Error": "Already subscribed to this feed.",
122- })
123 return118 return
124 }119 }
125 s.logger.Error("failed to create subscription", "error", err)120 s.logger.Error("failed to create subscription", "error", err)
@@ -127,10 +122,18 @@ func (s *Server) handleAddFeed(w http.ResponseWriter, r *http.Request) {
127 return122 return
128 }123 }
129 124
130- subs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 100, 0)125+ sub, _ := s.db.GetSubscription(r.Context(), user.DID, feedURL)
131- s.render(w, r, "feed_list.html", map[string]any{126+ if sub == nil {
132- "User": user,127+ w.WriteHeader(http.StatusNoContent)
133- "Subscriptions": subs,128+ return
129+ }
130+ s.render(w, r, "feed-item.html", map[string]any{
131+ "User": user,
132+ "FeedURL": sub.FeedURL,
133+ "FeedTitle": sub.FeedTitle,
134+ "Category": sub.Category,
135+ "FaviconURL": sub.FaviconURL,
136+ "UnreadCount": sub.UnreadCount,
134 })137 })
135 }138 }
136 139
@@ -313,7 +316,7 @@ func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) {
313 }316 }
314 317
315 subs, _ = s.db.ListSubscriptions(r.Context(), user.DID, "", 100, 0)318 subs, _ = s.db.ListSubscriptions(r.Context(), user.DID, "", 100, 0)
316- s.render(w, r, "feeds.html", map[string]any{319+ s.render(w, r, "feed-list.html", map[string]any{
317 "User": user,320 "User": user,
318 "Subscriptions": subs,321 "Subscriptions": subs,
319 })322 })
modified internal/tmpl/feeds.html +1 -21
@@ -61,27 +61,7 @@
6161
6262 <div id="feed-list" class="bg-spot-surface rounded-xl divide-y divide-spot-divider">
6363 {{range .Subscriptions}}
64- <div class="feed-item px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">
65- <a href="/articles?feed={{.FeedURL}}" class="min-w-0 flex-1 flex items-center gap-3">
66- {{if .FaviconURL.Valid}}<img src="{{.FaviconURL.String}}" class="w-5 h-5 rounded shrink-0" loading="lazy">{{else}}<span class="shrink-0 w-5 h-5 flex items-center justify-center text-spot-muted"><svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">{{template "icon-globe"}}</svg></span>{{end}}
67- <div class="min-w-0 flex-1">
68- <div class="flex items-center gap-2">
69- <span class="font-bold text-spot-text truncate">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
70- {{if .Category.Valid}}<span class="text-xs bg-spot-hover text-spot-secondary px-2 py-0.5 rounded-full shrink-0">{{.Category.String}}</span>{{end}}
71- </div>
72- <div class="text-xs text-spot-muted truncate mt-0.5">{{.FeedURL}}</div>
73- </div>
74- </a>
75- <div class="flex items-center gap-3">
76- {{if .UnreadCount}}<span class="text-xs bg-spot-green/20 text-spot-green px-2.5 py-0.5 rounded-full font-bold">{{.UnreadCount}}</span>{{end}}
77- <form hx-delete="/feeds/remove" hx-target="closest .feed-item" hx-swap="outerHTML swap:0.3s"
78- hx-confirm="Unsubscribe from this feed?" class="inline">
79- {{csrfInput $.CSRFToken}}
80- <input type="hidden" name="url" value="{{.FeedURL}}">
81- <button type="submit" class="text-sm text-spot-secondary hover:text-spot-red transition">Unsubscribe</button>
82- </form>
83- </div>
84- </div>
64+ {{template "feed-item.html" dict "FeedURL" .FeedURL "FeedTitle" .FeedTitle "Category" .Category "FaviconURL" .FaviconURL "UnreadCount" .UnreadCount "CSRFToken" $.CSRFToken}}
8565 {{else}}
8666 <div class="flex flex-col items-center justify-center py-12 px-6 text-center">
8767 <div class="w-14 h-14 rounded-full bg-spot-hover flex items-center justify-center mb-4 mx-auto">
@@ -61,27 +61,7 @@
61 61
62 <div id="feed-list" class="bg-spot-surface rounded-xl divide-y divide-spot-divider">62 <div id="feed-list" class="bg-spot-surface rounded-xl divide-y divide-spot-divider">
63 {{range .Subscriptions}}63 {{range .Subscriptions}}
64- <div class="feed-item px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">64+ {{template "feed-item.html" dict "FeedURL" .FeedURL "FeedTitle" .FeedTitle "Category" .Category "FaviconURL" .FaviconURL "UnreadCount" .UnreadCount "CSRFToken" $.CSRFToken}}
65- <a href="/articles?feed={{.FeedURL}}" class="min-w-0 flex-1 flex items-center gap-3">
66- {{if .FaviconURL.Valid}}<img src="{{.FaviconURL.String}}" class="w-5 h-5 rounded shrink-0" loading="lazy">{{else}}<span class="shrink-0 w-5 h-5 flex items-center justify-center text-spot-muted"><svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">{{template "icon-globe"}}</svg></span>{{end}}
67- <div class="min-w-0 flex-1">
68- <div class="flex items-center gap-2">
69- <span class="font-bold text-spot-text truncate">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
70- {{if .Category.Valid}}<span class="text-xs bg-spot-hover text-spot-secondary px-2 py-0.5 rounded-full shrink-0">{{.Category.String}}</span>{{end}}
71- </div>
72- <div class="text-xs text-spot-muted truncate mt-0.5">{{.FeedURL}}</div>
73- </div>
74- </a>
75- <div class="flex items-center gap-3">
76- {{if .UnreadCount}}<span class="text-xs bg-spot-green/20 text-spot-green px-2.5 py-0.5 rounded-full font-bold">{{.UnreadCount}}</span>{{end}}
77- <form hx-delete="/feeds/remove" hx-target="closest .feed-item" hx-swap="outerHTML swap:0.3s"
78- hx-confirm="Unsubscribe from this feed?" class="inline">
79- {{csrfInput $.CSRFToken}}
80- <input type="hidden" name="url" value="{{.FeedURL}}">
81- <button type="submit" class="text-sm text-spot-secondary hover:text-spot-red transition">Unsubscribe</button>
82- </form>
83- </div>
84- </div>
85 {{else}}65 {{else}}
86 <div class="flex flex-col items-center justify-center py-12 px-6 text-center">66 <div class="flex flex-col items-center justify-center py-12 px-6 text-center">
87 <div class="w-14 h-14 rounded-full bg-spot-hover flex items-center justify-center mb-4 mx-auto">67 <div class="w-14 h-14 rounded-full bg-spot-hover flex items-center justify-center mb-4 mx-auto">
added internal/tmpl/partials/feed-item.html +23 -0
new file mode 100644
@@ -0,0 +1,23 @@
1+{{define "feed-item.html"}}
2+<div class="feed-item px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">
3+ <a href="/articles?feed={{.FeedURL}}" class="min-w-0 flex-1 flex items-center gap-3">
4+ {{if .FaviconURL.Valid}}<img src="{{.FaviconURL.String}}" class="w-5 h-5 rounded shrink-0" loading="lazy">{{else}}<span class="shrink-0 w-5 h-5 flex items-center justify-center text-spot-muted"><svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">{{template "icon-globe"}}</svg></span>{{end}}
5+ <div class="min-w-0 flex-1">
6+ <div class="flex items-center gap-2">
7+ <span class="font-bold text-spot-text truncate">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
8+ {{if .Category.Valid}}<span class="text-xs bg-spot-hover text-spot-secondary px-2 py-0.5 rounded-full shrink-0">{{.Category.String}}</span>{{end}}
9+ </div>
10+ <div class="text-xs text-spot-muted truncate mt-0.5">{{.FeedURL}}</div>
11+ </div>
12+ </a>
13+ <div class="flex items-center gap-3">
14+ {{if .UnreadCount}}<span class="text-xs bg-spot-green/20 text-spot-green px-2.5 py-0.5 rounded-full font-bold">{{.UnreadCount}}</span>{{end}}
15+ <form hx-delete="/feeds/remove" hx-target="closest .feed-item" hx-swap="outerHTML swap:0.3s"
16+ hx-confirm="Unsubscribe from this feed?" class="inline">
17+ {{csrfInput .CSRFToken}}
18+ <input type="hidden" name="url" value="{{.FeedURL}}">
19+ <button type="submit" class="text-sm text-spot-secondary hover:text-spot-red transition">Unsubscribe</button>
20+ </form>
21+ </div>
22+</div>
23+{{end}}
new file mode 100644
@@ -0,0 +1,23 @@
1+{{define "feed-item.html"}}
2+<div class="feed-item px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">
3+ <a href="/articles?feed={{.FeedURL}}" class="min-w-0 flex-1 flex items-center gap-3">
4+ {{if .FaviconURL.Valid}}<img src="{{.FaviconURL.String}}" class="w-5 h-5 rounded shrink-0" loading="lazy">{{else}}<span class="shrink-0 w-5 h-5 flex items-center justify-center text-spot-muted"><svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">{{template "icon-globe"}}</svg></span>{{end}}
5+ <div class="min-w-0 flex-1">
6+ <div class="flex items-center gap-2">
7+ <span class="font-bold text-spot-text truncate">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
8+ {{if .Category.Valid}}<span class="text-xs bg-spot-hover text-spot-secondary px-2 py-0.5 rounded-full shrink-0">{{.Category.String}}</span>{{end}}
9+ </div>
10+ <div class="text-xs text-spot-muted truncate mt-0.5">{{.FeedURL}}</div>
11+ </div>
12+ </a>
13+ <div class="flex items-center gap-3">
14+ {{if .UnreadCount}}<span class="text-xs bg-spot-green/20 text-spot-green px-2.5 py-0.5 rounded-full font-bold">{{.UnreadCount}}</span>{{end}}
15+ <form hx-delete="/feeds/remove" hx-target="closest .feed-item" hx-swap="outerHTML swap:0.3s"
16+ hx-confirm="Unsubscribe from this feed?" class="inline">
17+ {{csrfInput .CSRFToken}}
18+ <input type="hidden" name="url" value="{{.FeedURL}}">
19+ <button type="submit" class="text-sm text-spot-secondary hover:text-spot-red transition">Unsubscribe</button>
20+ </form>
21+ </div>
22+</div>
23+{{end}}
added internal/tmpl/partials/feed-list.html +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+{{define "feed-list.html"}}
2+{{range .Subscriptions}}
3+{{template "feed-item.html" dict "FeedURL" .FeedURL "FeedTitle" .FeedTitle "Category" .Category "FaviconURL" .FaviconURL "UnreadCount" .UnreadCount "CSRFToken" $.CSRFToken}}
4+{{else}}
5+<div class="flex flex-col items-center justify-center py-12 px-6 text-center">
6+ <div class="w-14 h-14 rounded-full bg-spot-hover flex items-center justify-center mb-4 mx-auto">
7+ <svg class="w-7 h-7 text-spot-muted" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 5c7.18 0 13 5.82 13 13M6 11a7 7 0 017 7m-7-1a1 1 0 11-2 0 1 1 0 012 0z"/></svg>
8+ </div>
9+ <p class="text-sm text-spot-secondary mb-1">No feeds yet</p>
10+ <p class="text-xs text-spot-muted">Add one from the sidebar to get started.</p>
11+</div>
12+{{end}}
13+{{end}}
new file mode 100644
@@ -0,0 +1,13 @@
1+{{define "feed-list.html"}}
2+{{range .Subscriptions}}
3+{{template "feed-item.html" dict "FeedURL" .FeedURL "FeedTitle" .FeedTitle "Category" .Category "FaviconURL" .FaviconURL "UnreadCount" .UnreadCount "CSRFToken" $.CSRFToken}}
4+{{else}}
5+<div class="flex flex-col items-center justify-center py-12 px-6 text-center">
6+ <div class="w-14 h-14 rounded-full bg-spot-hover flex items-center justify-center mb-4 mx-auto">
7+ <svg class="w-7 h-7 text-spot-muted" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 5c7.18 0 13 5.82 13 13M6 11a7 7 0 017 7m-7-1a1 1 0 11-2 0 1 1 0 012 0z"/></svg>
8+ </div>
9+ <p class="text-sm text-spot-secondary mb-1">No feeds yet</p>
10+ <p class="text-xs text-spot-muted">Add one from the sidebar to get started.</p>
11+</div>
12+{{end}}
13+{{end}}
modified static/input.css +5 -0
@@ -94,3 +94,8 @@ body {
9494 ::-webkit-scrollbar-track { background: var(--spot-bg); }
9595 ::-webkit-scrollbar-thumb { background: var(--spot-muted); border-radius: 4px; }
9696 ::-webkit-scrollbar-thumb:hover { background: var(--spot-outline); }
97+
98+.htmx-swapping {
99+ opacity: 0;
100+ transition: opacity 0.3s ease-out;
101+}
@@ -94,3 +94,8 @@ body {
94 ::-webkit-scrollbar-track { background: var(--spot-bg); }94 ::-webkit-scrollbar-track { background: var(--spot-bg); }
95 ::-webkit-scrollbar-thumb { background: var(--spot-muted); border-radius: 4px; }95 ::-webkit-scrollbar-thumb { background: var(--spot-muted); border-radius: 4px; }
96 ::-webkit-scrollbar-thumb:hover { background: var(--spot-outline); }96 ::-webkit-scrollbar-thumb:hover { background: var(--spot-outline); }
97+
98+.htmx-swapping {
99+ opacity: 0;
100+ transition: opacity 0.3s ease-out;
101+}