@rmdes/indiekit-endpoint-rss 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,289 @@
1
+ /* RSS endpoint styles */
2
+
3
+ /* Stats grid */
4
+ .rss-stats {
5
+ display: grid;
6
+ gap: var(--space-s);
7
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
8
+ }
9
+
10
+ .rss-stat {
11
+ align-items: center;
12
+ background: var(--color-offset);
13
+ border-radius: var(--radius-m);
14
+ display: flex;
15
+ flex-direction: column;
16
+ padding: var(--space-s);
17
+ text-align: center;
18
+ }
19
+
20
+ .rss-stat__value {
21
+ color: var(--color-accent);
22
+ font-size: var(--step-2);
23
+ font-weight: var(--font-weight-bold);
24
+ }
25
+
26
+ .rss-stat__label {
27
+ color: var(--color-text-secondary);
28
+ font-size: var(--step--2);
29
+ letter-spacing: 0.05em;
30
+ text-transform: uppercase;
31
+ }
32
+
33
+ /* Sync status bar */
34
+ .rss-sync-bar {
35
+ align-items: center;
36
+ display: flex;
37
+ flex-wrap: wrap;
38
+ gap: var(--space-m);
39
+ justify-content: space-between;
40
+ }
41
+
42
+ .rss-sync-info {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: var(--space-3xs);
46
+ }
47
+
48
+ .rss-sync-label {
49
+ color: var(--color-text-secondary);
50
+ font-size: var(--step--2);
51
+ text-transform: uppercase;
52
+ }
53
+
54
+ .rss-sync-time {
55
+ font-weight: var(--font-weight-medium);
56
+ }
57
+
58
+ .rss-syncing {
59
+ align-items: center;
60
+ color: var(--color-accent);
61
+ display: inline-flex;
62
+ gap: var(--space-xs);
63
+ }
64
+
65
+ .rss-spinner {
66
+ animation: rss-spin 0.8s linear infinite;
67
+ border: 2px solid currentColor;
68
+ border-radius: 50%;
69
+ border-top-color: transparent;
70
+ height: 16px;
71
+ width: 16px;
72
+ }
73
+
74
+ @keyframes rss-spin {
75
+ to { transform: rotate(360deg); }
76
+ }
77
+
78
+ /* Feed list */
79
+ .rss-feed-list {
80
+ list-style: none;
81
+ margin: 0;
82
+ padding: 0;
83
+ }
84
+
85
+ .rss-feed-item {
86
+ align-items: center;
87
+ border-block-end: 1px solid var(--color-border);
88
+ display: flex;
89
+ gap: var(--space-s);
90
+ padding: var(--space-s) 0;
91
+ }
92
+
93
+ .rss-feed-item:last-child {
94
+ border-block-end: none;
95
+ }
96
+
97
+ .rss-feed-icon {
98
+ border-radius: var(--radius-s);
99
+ flex-shrink: 0;
100
+ height: 32px;
101
+ object-fit: cover;
102
+ width: 32px;
103
+ }
104
+
105
+ .rss-feed-icon-placeholder {
106
+ align-items: center;
107
+ background: var(--color-border);
108
+ border-radius: var(--radius-s);
109
+ color: var(--color-text-secondary);
110
+ display: flex;
111
+ flex-shrink: 0;
112
+ font-size: var(--step-0);
113
+ height: 32px;
114
+ justify-content: center;
115
+ width: 32px;
116
+ }
117
+
118
+ .rss-feed-info {
119
+ flex: 1;
120
+ min-width: 0;
121
+ }
122
+
123
+ .rss-feed-title {
124
+ font-weight: var(--font-weight-medium);
125
+ overflow: hidden;
126
+ text-overflow: ellipsis;
127
+ white-space: nowrap;
128
+ }
129
+
130
+ .rss-feed-meta {
131
+ color: var(--color-text-secondary);
132
+ display: flex;
133
+ font-size: var(--step--2);
134
+ gap: var(--space-xs);
135
+ }
136
+
137
+ .rss-feed-actions {
138
+ align-items: center;
139
+ display: flex;
140
+ gap: var(--space-xs);
141
+ }
142
+
143
+ .rss-feed-error {
144
+ color: #d32f2f;
145
+ font-size: var(--step--2);
146
+ }
147
+
148
+ /* Item list */
149
+ .rss-item-list {
150
+ list-style: none;
151
+ margin: 0;
152
+ padding: 0;
153
+ }
154
+
155
+ .rss-item {
156
+ border-block-end: 1px solid var(--color-border);
157
+ display: flex;
158
+ gap: var(--space-s);
159
+ padding: var(--space-s) 0;
160
+ }
161
+
162
+ .rss-item:last-child {
163
+ border-block-end: none;
164
+ }
165
+
166
+ .rss-item-image {
167
+ border-radius: var(--radius-s);
168
+ flex-shrink: 0;
169
+ height: 60px;
170
+ object-fit: cover;
171
+ width: 60px;
172
+ }
173
+
174
+ .rss-item-placeholder {
175
+ background: var(--color-border);
176
+ border-radius: var(--radius-s);
177
+ flex-shrink: 0;
178
+ height: 60px;
179
+ width: 60px;
180
+ }
181
+
182
+ .rss-item-info {
183
+ flex: 1;
184
+ min-width: 0;
185
+ }
186
+
187
+ .rss-item-title {
188
+ color: inherit;
189
+ display: block;
190
+ font-weight: var(--font-weight-medium);
191
+ overflow: hidden;
192
+ text-decoration: none;
193
+ text-overflow: ellipsis;
194
+ white-space: nowrap;
195
+ }
196
+
197
+ .rss-item-title:hover {
198
+ text-decoration: underline;
199
+ }
200
+
201
+ .rss-item-description {
202
+ color: var(--color-text-secondary);
203
+ display: -webkit-box;
204
+ font-size: var(--step--1);
205
+ margin: var(--space-3xs) 0;
206
+ overflow: hidden;
207
+ -webkit-box-orient: vertical;
208
+ -webkit-line-clamp: 2;
209
+ }
210
+
211
+ .rss-item-meta {
212
+ color: var(--color-text-secondary);
213
+ font-size: var(--step--2);
214
+ }
215
+
216
+ /* Toggle switch */
217
+ .rss-toggle {
218
+ cursor: pointer;
219
+ height: 22px;
220
+ position: relative;
221
+ width: 40px;
222
+ }
223
+
224
+ .rss-toggle input {
225
+ height: 0;
226
+ opacity: 0;
227
+ width: 0;
228
+ }
229
+
230
+ .rss-toggle-slider {
231
+ background-color: #ccc;
232
+ border-radius: 22px;
233
+ bottom: 0;
234
+ left: 0;
235
+ position: absolute;
236
+ right: 0;
237
+ top: 0;
238
+ transition: 0.3s;
239
+ }
240
+
241
+ .rss-toggle-slider::before {
242
+ background-color: white;
243
+ border-radius: 50%;
244
+ bottom: 3px;
245
+ content: "";
246
+ height: 16px;
247
+ left: 3px;
248
+ position: absolute;
249
+ transition: 0.3s;
250
+ width: 16px;
251
+ }
252
+
253
+ .rss-toggle input:checked + .rss-toggle-slider {
254
+ background-color: var(--color-accent);
255
+ }
256
+
257
+ .rss-toggle input:checked + .rss-toggle-slider::before {
258
+ transform: translateX(18px);
259
+ }
260
+
261
+ /* Delete button */
262
+ .rss-delete-btn {
263
+ background: none;
264
+ border: none;
265
+ color: #d32f2f;
266
+ cursor: pointer;
267
+ font-size: var(--step-0);
268
+ opacity: 0.6;
269
+ padding: var(--space-3xs);
270
+ }
271
+
272
+ .rss-delete-btn:hover {
273
+ opacity: 1;
274
+ }
275
+
276
+ /* Public link */
277
+ .rss-public-link {
278
+ align-items: center;
279
+ display: flex;
280
+ flex-wrap: wrap;
281
+ gap: var(--space-m);
282
+ justify-content: space-between;
283
+ }
284
+
285
+ .rss-public-link p {
286
+ color: var(--color-text-secondary);
287
+ font-size: var(--step--1);
288
+ margin: 0;
289
+ }
@@ -1,6 +1,22 @@
1
1
  import { getSyncState, runSync } from "../sync.js";
2
2
  import { formatFeed, formatItem } from "../utils.js";
3
3
 
4
+ /**
5
+ * Extract and clear flash messages from session
6
+ * Returns { success, error } for Indiekit's native notificationBanner
7
+ */
8
+ function consumeFlashMessage(request) {
9
+ const result = {};
10
+ if (request.session?.messages?.length) {
11
+ const msg = request.session.messages[0];
12
+ if (msg.type === "success") result.success = msg.content;
13
+ else if (msg.type === "error" || msg.type === "warning")
14
+ result.error = msg.content;
15
+ request.session.messages = null;
16
+ }
17
+ return result;
18
+ }
19
+
4
20
  export const dashboardController = {
5
21
  /**
6
22
  * Render admin dashboard
@@ -41,6 +57,9 @@ export const dashboardController = {
41
57
 
42
58
  const syncState = getSyncState();
43
59
 
60
+ // Extract flash messages for native Indiekit notification banner
61
+ const flash = consumeFlashMessage(request);
62
+
44
63
  response.render("rss", {
45
64
  title: response.__("rss.title"),
46
65
  feeds: feeds.map(formatFeed),
@@ -54,12 +73,7 @@ export const dashboardController = {
54
73
  },
55
74
  mountPath: request.baseUrl,
56
75
  publicUrl: rssEndpoint,
57
- synced: request.query.synced,
58
- syncedFeeds: request.query.feeds,
59
- syncedItems: request.query.items,
60
- syncedPruned: request.query.pruned,
61
- syncInProgress: request.query.syncing,
62
- queryError: request.query.error,
76
+ ...flash,
63
77
  });
64
78
  } catch (error) {
65
79
  next(error);
@@ -128,45 +142,61 @@ export const dashboardController = {
128
142
  const { rssConfig, getRssDb } = request.app.locals.application;
129
143
 
130
144
  if (!rssConfig) {
131
- return response.redirect(
132
- request.baseUrl + "?error=" + encodeURIComponent("Not configured"),
133
- );
145
+ request.session.messages = [
146
+ { type: "error", content: "Not configured" },
147
+ ];
148
+ return response.redirect(request.baseUrl);
134
149
  }
135
150
 
136
151
  const db = getRssDb?.();
137
152
  if (!db) {
138
- return response.redirect(
139
- request.baseUrl +
140
- "?error=" +
141
- encodeURIComponent("Database not available"),
142
- );
153
+ request.session.messages = [
154
+ { type: "error", content: "Database not available" },
155
+ ];
156
+ return response.redirect(request.baseUrl);
143
157
  }
144
158
 
145
159
  const syncState = getSyncState();
146
160
  if (syncState.syncing) {
147
- return response.redirect(request.baseUrl + "?syncing=true");
161
+ request.session.messages = [
162
+ { type: "warning", content: "A sync is already in progress" },
163
+ ];
164
+ return response.redirect(request.baseUrl);
148
165
  }
149
166
 
150
167
  const result = await runSync(db, rssConfig);
151
168
 
152
169
  if (result.error) {
153
- return response.redirect(
154
- request.baseUrl + "?error=" + encodeURIComponent(result.error),
155
- );
170
+ request.session.messages = [
171
+ { type: "error", content: result.error },
172
+ ];
173
+ return response.redirect(request.baseUrl);
156
174
  }
157
175
 
158
- const params = new URLSearchParams({
159
- synced: "true",
160
- feeds: result.feedsProcessed || 0,
161
- items: result.itemsAdded || 0,
162
- pruned: result.itemsPruned || 0,
163
- });
164
- response.redirect(request.baseUrl + "?" + params.toString());
176
+ const itemsAdded = result.itemsAdded || 0;
177
+ const feedsProcessed = result.feedsProcessed || 0;
178
+ const itemsPruned = result.itemsPruned || 0;
179
+
180
+ let message;
181
+ if (itemsAdded > 0) {
182
+ message = `Synced ${itemsAdded} new items from ${feedsProcessed} feeds`;
183
+ if (itemsPruned > 0) {
184
+ message += ` (${itemsPruned} old items pruned)`;
185
+ }
186
+ } else {
187
+ message = "Feeds are up to date, nothing new to sync";
188
+ }
189
+
190
+ request.session.messages = [
191
+ { type: "success", content: message },
192
+ ];
193
+ response.redirect(request.baseUrl);
165
194
  } catch (error) {
166
195
  console.error("[RSS] Sync error:", error.message);
167
- response.redirect(
168
- request.baseUrl + "?error=" + encodeURIComponent(error.message),
169
- );
196
+ request.session.messages = [
197
+ { type: "error", content: error.message },
198
+ ];
199
+ response.redirect(request.baseUrl);
170
200
  }
171
201
  },
172
202
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-rss",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "RSS feed reader endpoint for Indiekit. Aggregates multiple feeds, caches in MongoDB, displays on frontend.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -34,6 +34,7 @@
34
34
  ".": "./index.js"
35
35
  },
36
36
  "files": [
37
+ "assets",
37
38
  "lib",
38
39
  "locales",
39
40
  "views",
@@ -0,0 +1,6 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ <link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-rss/styles.css">
5
+ {% block rss %}{% endblock %}
6
+ {% endblock %}
package/views/rss.njk CHANGED
@@ -1,346 +1,9 @@
1
- {% extends "document.njk" %}
1
+ {% extends "layouts/rss.njk" %}
2
2
 
3
- {% block content %}
4
- <style>
5
- .rss-dashboard {
6
- display: flex;
7
- flex-direction: column;
8
- gap: var(--space-xl, 2rem);
9
- }
10
-
11
- .rss-section {
12
- background: var(--color-offset, #f5f5f5);
13
- border-radius: var(--border-radius-small, 0.5rem);
14
- padding: var(--space-m, 1.5rem);
15
- }
16
-
17
- .rss-section h2 {
18
- font: var(--font-heading, bold 1.25rem/1.4 sans-serif);
19
- margin-block-end: var(--space-s, 0.75rem);
20
- padding-block-end: var(--space-xs, 0.5rem);
21
- border-block-end: 1px solid var(--color-outline-variant, #ddd);
22
- }
23
-
24
- /* Notifications */
25
- .rss-notification {
26
- border-radius: var(--border-radius-small, 0.25rem);
27
- padding: var(--space-s, 0.75rem) var(--space-m, 1rem);
28
- margin-block-end: var(--space-s, 0.75rem);
29
- }
30
- .rss-notification--success {
31
- background: var(--color-success, #d4edda);
32
- color: var(--color-on-success, #155724);
33
- }
34
- .rss-notification--warning {
35
- background: #fff3cd;
36
- color: #856404;
37
- }
38
- .rss-notification--error {
39
- background: var(--color-error, #f8d7da);
40
- color: var(--color-on-error, #721c24);
41
- }
42
-
43
- /* Stats grid */
44
- .rss-stats-grid {
45
- display: grid;
46
- grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
47
- gap: var(--space-s, 0.75rem);
48
- }
49
- .rss-stat {
50
- display: flex;
51
- flex-direction: column;
52
- align-items: center;
53
- padding: var(--space-s, 0.75rem);
54
- background: var(--color-background, #fff);
55
- border-radius: var(--border-radius-small, 0.5rem);
56
- text-align: center;
57
- }
58
- .rss-stat-value {
59
- font-size: 1.5rem;
60
- font-weight: 700;
61
- color: var(--color-accent, #ff6600);
62
- }
63
- .rss-stat-label {
64
- font-size: 0.75rem;
65
- color: var(--color-on-offset, #666);
66
- text-transform: uppercase;
67
- letter-spacing: 0.05em;
68
- }
69
-
70
- /* Sync status */
71
- .rss-sync-bar {
72
- display: flex;
73
- align-items: center;
74
- justify-content: space-between;
75
- gap: var(--space-m, 1rem);
76
- flex-wrap: wrap;
77
- }
78
- .rss-sync-info {
79
- display: flex;
80
- flex-direction: column;
81
- gap: 0.25rem;
82
- }
83
- .rss-sync-label {
84
- font-size: 0.75rem;
85
- color: var(--color-on-offset, #666);
86
- text-transform: uppercase;
87
- }
88
- .rss-sync-time {
89
- font-weight: 500;
90
- }
91
- .rss-syncing {
92
- display: inline-flex;
93
- align-items: center;
94
- gap: 0.5rem;
95
- color: var(--color-accent, #ff6600);
96
- }
97
- .rss-spinner {
98
- width: 16px;
99
- height: 16px;
100
- border: 2px solid currentColor;
101
- border-top-color: transparent;
102
- border-radius: 50%;
103
- animation: rss-spin 0.8s linear infinite;
104
- }
105
- @keyframes rss-spin {
106
- to { transform: rotate(360deg); }
107
- }
108
-
109
- /* Add feed form */
110
- .rss-add-form {
111
- display: flex;
112
- gap: 0.5rem;
113
- }
114
- .rss-add-form input {
115
- flex: 1;
116
- appearance: none;
117
- background-color: var(--color-background, #fff);
118
- border: 1px solid var(--color-outline-variant, #ccc);
119
- border-radius: var(--border-radius-small, 0.25rem);
120
- font: var(--font-body, 0.875rem/1.4 sans-serif);
121
- padding: calc(var(--space-s, 0.75rem) / 2) var(--space-s, 0.75rem);
122
- }
123
- .rss-add-form input:focus {
124
- border-color: var(--color-primary, #0066cc);
125
- outline: 2px solid var(--color-primary, #0066cc);
126
- outline-offset: 1px;
127
- }
128
-
129
- /* Feed list */
130
- .rss-feed-list {
131
- list-style: none;
132
- padding: 0;
133
- margin: 0;
134
- }
135
- .rss-feed-item {
136
- display: flex;
137
- align-items: center;
138
- gap: 0.75rem;
139
- padding: 0.75rem 0;
140
- border-bottom: 1px solid var(--color-outline-variant, #eee);
141
- }
142
- .rss-feed-item:last-child { border-bottom: none; }
143
- .rss-feed-icon {
144
- width: 32px;
145
- height: 32px;
146
- object-fit: cover;
147
- border-radius: 0.25rem;
148
- flex-shrink: 0;
149
- }
150
- .rss-feed-icon-placeholder {
151
- width: 32px;
152
- height: 32px;
153
- background: var(--color-outline-variant, #ddd);
154
- border-radius: 0.25rem;
155
- flex-shrink: 0;
156
- display: flex;
157
- align-items: center;
158
- justify-content: center;
159
- color: var(--color-on-offset, #666);
160
- font-size: 1rem;
161
- }
162
- .rss-feed-info {
163
- flex: 1;
164
- min-width: 0;
165
- }
166
- .rss-feed-title {
167
- font-weight: 500;
168
- white-space: nowrap;
169
- overflow: hidden;
170
- text-overflow: ellipsis;
171
- }
172
- .rss-feed-meta {
173
- display: flex;
174
- gap: 0.5rem;
175
- font-size: 0.75rem;
176
- color: var(--color-on-offset, #666);
177
- }
178
- .rss-feed-actions {
179
- display: flex;
180
- gap: 0.5rem;
181
- align-items: center;
182
- }
183
- .rss-feed-error {
184
- color: #d32f2f;
185
- font-size: 0.75rem;
186
- }
187
-
188
- /* Item list */
189
- .rss-item-list {
190
- list-style: none;
191
- padding: 0;
192
- margin: 0;
193
- }
194
- .rss-item {
195
- display: flex;
196
- gap: 0.75rem;
197
- padding: 0.75rem 0;
198
- border-bottom: 1px solid var(--color-outline-variant, #eee);
199
- }
200
- .rss-item:last-child { border-bottom: none; }
201
- .rss-item-image {
202
- width: 60px;
203
- height: 60px;
204
- object-fit: cover;
205
- border-radius: 0.25rem;
206
- flex-shrink: 0;
207
- }
208
- .rss-item-placeholder {
209
- width: 60px;
210
- height: 60px;
211
- background: var(--color-outline-variant, #ddd);
212
- border-radius: 0.25rem;
213
- flex-shrink: 0;
214
- }
215
- .rss-item-info {
216
- flex: 1;
217
- min-width: 0;
218
- }
219
- .rss-item-title {
220
- font-weight: 500;
221
- text-decoration: none;
222
- color: inherit;
223
- display: block;
224
- white-space: nowrap;
225
- overflow: hidden;
226
- text-overflow: ellipsis;
227
- }
228
- .rss-item-title:hover { text-decoration: underline; }
229
- .rss-item-description {
230
- font-size: 0.875rem;
231
- color: var(--color-on-offset, #666);
232
- margin: 0.25rem 0;
233
- display: -webkit-box;
234
- -webkit-line-clamp: 2;
235
- -webkit-box-orient: vertical;
236
- overflow: hidden;
237
- }
238
- .rss-item-meta {
239
- font-size: 0.75rem;
240
- color: var(--color-on-offset, #666);
241
- }
242
-
243
- /* Toggle switch */
244
- .rss-toggle {
245
- position: relative;
246
- width: 40px;
247
- height: 22px;
248
- cursor: pointer;
249
- }
250
- .rss-toggle input {
251
- opacity: 0;
252
- width: 0;
253
- height: 0;
254
- }
255
- .rss-toggle-slider {
256
- position: absolute;
257
- top: 0; left: 0; right: 0; bottom: 0;
258
- background-color: #ccc;
259
- border-radius: 22px;
260
- transition: 0.3s;
261
- }
262
- .rss-toggle-slider:before {
263
- position: absolute;
264
- content: "";
265
- height: 16px;
266
- width: 16px;
267
- left: 3px;
268
- bottom: 3px;
269
- background-color: white;
270
- border-radius: 50%;
271
- transition: 0.3s;
272
- }
273
- .rss-toggle input:checked + .rss-toggle-slider {
274
- background-color: var(--color-accent, #ff6600);
275
- }
276
- .rss-toggle input:checked + .rss-toggle-slider:before {
277
- transform: translateX(18px);
278
- }
279
-
280
- /* Delete button */
281
- .rss-delete-btn {
282
- background: none;
283
- border: none;
284
- color: #d32f2f;
285
- cursor: pointer;
286
- padding: 0.25rem;
287
- font-size: 1rem;
288
- opacity: 0.6;
289
- }
290
- .rss-delete-btn:hover { opacity: 1; }
291
-
292
- /* Public link */
293
- .rss-public-link {
294
- display: flex;
295
- align-items: center;
296
- justify-content: space-between;
297
- gap: var(--space-m, 1rem);
298
- flex-wrap: wrap;
299
- }
300
- .rss-public-link p {
301
- margin: 0;
302
- color: var(--color-on-offset, #666);
303
- font: var(--font-caption, 0.875rem/1.4 sans-serif);
304
- }
305
- </style>
306
-
307
- {# Notifications #}
308
- {% if synced %}
309
- <div class="rss-notification rss-notification--success">
310
- {% if syncedItems and syncedItems != "0" %}
311
- Synced {{ syncedItems }} new items from {{ syncedFeeds }} feeds
312
- {% else %}
313
- Feeds are up to date, nothing new to sync
314
- {% endif %}
315
- {% if syncedPruned and syncedPruned != "0" %}
316
- ({{ syncedPruned }} old items pruned)
317
- {% endif %}
318
- </div>
319
- {% endif %}
320
-
321
- {% if syncInProgress %}
322
- <div class="rss-notification rss-notification--warning">
323
- A sync is already in progress, please wait
324
- </div>
325
- {% endif %}
326
-
327
- {% if queryError %}
328
- <div class="rss-notification rss-notification--error">
329
- {{ queryError }}
330
- </div>
331
- {% endif %}
332
-
333
- {% if configError %}
334
- <div class="rss-notification rss-notification--error">
335
- {{ configError }}
336
- </div>
337
- {% endif %}
338
-
339
- <div class="rss-dashboard">
3
+ {% block rss %}
340
4
  {% if not configError %}
341
5
  {# Sync Status & Actions #}
342
- <section class="rss-section">
343
- <h2>{{ __("rss.syncStatus") }}</h2>
6
+ {% call section({ title: __("rss.syncStatus") }) %}
344
7
  <div class="rss-sync-bar">
345
8
  <div class="rss-sync-info">
346
9
  <span class="rss-sync-label">{{ __("rss.lastSynced") }}</span>
@@ -362,58 +25,62 @@
362
25
  </div>
363
26
  <div class="button-group" style="display: flex; gap: 0.5rem;">
364
27
  <form action="{{ mountPath }}/sync" method="post" style="display: inline;">
365
- <button type="submit" class="button button--primary" {% if syncState.syncing %}disabled{% endif %}>
366
- {{ __("rss.syncNow") }}
367
- </button>
28
+ {{ button({
29
+ type: "submit",
30
+ text: __("rss.syncNow"),
31
+ disabled: syncState.syncing
32
+ }) }}
368
33
  </form>
369
34
  <form action="{{ mountPath }}/clear-resync" method="post" style="display: inline;" id="clear-resync-form">
370
- <button type="submit" class="button" {% if syncState.syncing %}disabled{% endif %}>
371
- {{ __("rss.clearResync") }}
372
- </button>
35
+ {{ button({
36
+ classes: "button--secondary",
37
+ type: "submit",
38
+ text: __("rss.clearResync"),
39
+ disabled: syncState.syncing
40
+ }) }}
373
41
  </form>
374
42
  </div>
375
43
  </div>
376
- </section>
44
+ {% endcall %}
377
45
 
378
46
  {# Stats #}
379
- <section class="rss-section">
380
- <h2>{{ __("rss.status") }}</h2>
381
- <div class="rss-stats-grid">
47
+ {% call section({ title: __("rss.status") }) %}
48
+ <div class="rss-stats">
382
49
  <div class="rss-stat">
383
- <span class="rss-stat-value">{{ totalFeeds }}</span>
384
- <span class="rss-stat-label">{{ __("rss.feedsCount") }}</span>
50
+ <span class="rss-stat__value">{{ totalFeeds }}</span>
51
+ <span class="rss-stat__label">{{ __("rss.feedsCount") }}</span>
385
52
  </div>
386
53
  <div class="rss-stat">
387
- <span class="rss-stat-value">{{ totalItems }}</span>
388
- <span class="rss-stat-label">{{ __("rss.itemsCount") }}</span>
54
+ <span class="rss-stat__value">{{ totalItems }}</span>
55
+ <span class="rss-stat__label">{{ __("rss.itemsCount") }}</span>
389
56
  </div>
390
57
  <div class="rss-stat">
391
- <span class="rss-stat-value">{{ feeds | selectattr("enabled") | list | length }}</span>
392
- <span class="rss-stat-label">{{ __("rss.enabled") }}</span>
58
+ <span class="rss-stat__value">{{ feeds | selectattr("enabled") | list | length }}</span>
59
+ <span class="rss-stat__label">{{ __("rss.enabled") }}</span>
393
60
  </div>
394
61
  </div>
395
- </section>
62
+ {% endcall %}
396
63
 
397
64
  {# Add Feed Form #}
398
- <section class="rss-section">
399
- <h2>{{ __("rss.addFeed") }}</h2>
400
- <form class="rss-add-form" action="{{ mountPath }}/api/feeds" method="post" id="add-feed-form">
65
+ {% call section({ title: __("rss.addFeed") }) %}
66
+ <form class="input-button-group" action="{{ mountPath }}/api/feeds" method="post" id="add-feed-form">
401
67
  <input
68
+ class="input"
402
69
  type="url"
403
70
  name="url"
404
71
  placeholder="{{ __("rss.feedUrlPlaceholder") }}"
405
72
  required
406
73
  >
407
- <button type="submit" class="button button--primary">
408
- {{ __("rss.addFeed") }}
409
- </button>
74
+ {{ button({
75
+ type: "submit",
76
+ text: __("rss.addFeed")
77
+ }) }}
410
78
  </form>
411
- </section>
79
+ {% endcall %}
412
80
 
413
81
  {# Feeds List #}
414
82
  {% if feeds and feeds.length > 0 %}
415
- <section class="rss-section">
416
- <h2>{{ __("rss.feeds") }}</h2>
83
+ {% call section({ title: __("rss.feeds") }) %}
417
84
  <ul class="rss-feed-list">
418
85
  {% for feed in feeds %}
419
86
  <li class="rss-feed-item" data-feed-id="{{ feed.id }}">
@@ -453,13 +120,12 @@
453
120
  </li>
454
121
  {% endfor %}
455
122
  </ul>
456
- </section>
123
+ {% endcall %}
457
124
  {% endif %}
458
125
 
459
126
  {# Recent Items #}
460
127
  {% if recentItems and recentItems.length > 0 %}
461
- <section class="rss-section">
462
- <h2>{{ __("rss.recentItems") }}</h2>
128
+ {% call section({ title: __("rss.recentItems") }) %}
463
129
  <ul class="rss-item-list">
464
130
  {% for item in recentItems %}
465
131
  <li class="rss-item">
@@ -485,20 +151,22 @@
485
151
  </li>
486
152
  {% endfor %}
487
153
  </ul>
488
- </section>
154
+ {% endcall %}
489
155
  {% endif %}
490
156
 
491
157
  {# Public Page Link #}
492
- <section class="rss-section">
158
+ {% call section({ title: __("rss.widget.title") if __("rss.widget.title") else "Public page" }) %}
493
159
  <div class="rss-public-link">
494
160
  <p>{{ __("rss.widget.description") }}</p>
495
- <a href="/news/" class="button button--secondary" target="_blank">
496
- {{ __("rss.widget.view") }}
497
- </a>
161
+ {{ button({
162
+ classes: "button--secondary",
163
+ href: "/news/",
164
+ text: __("rss.widget.view"),
165
+ target: "_blank"
166
+ }) }}
498
167
  </div>
499
- </section>
168
+ {% endcall %}
500
169
  {% endif %}
501
- </div>
502
170
 
503
171
  <script>
504
172
  // Handle add feed form