@rmdes/indiekit-endpoint-microsub 1.0.39 → 1.0.41
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.
- package/assets/styles.css +6 -0
- package/lib/controllers/reader.js +3 -2
- package/package.json +1 -1
- package/views/channel.njk +33 -0
- package/views/partials/item-card.njk +12 -0
- package/views/timeline.njk +88 -0
package/assets/styles.css
CHANGED
|
@@ -421,6 +421,12 @@
|
|
|
421
421
|
margin-left: auto;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
/* Save for later button */
|
|
425
|
+
.item-actions__save-later--saved {
|
|
426
|
+
color: var(--color-accent, #4a9eff);
|
|
427
|
+
opacity: 0.6;
|
|
428
|
+
}
|
|
429
|
+
|
|
424
430
|
/* ==========================================================================
|
|
425
431
|
Single Item View
|
|
426
432
|
========================================================================== */
|
|
@@ -1184,10 +1184,10 @@ export async function timeline(request, response) {
|
|
|
1184
1184
|
// Get channels with colors for filtering UI and item decoration
|
|
1185
1185
|
const channelList = await getChannelsWithColors(application, userId);
|
|
1186
1186
|
|
|
1187
|
-
// Build channel lookup map (ObjectId string -> { name, color })
|
|
1187
|
+
// Build channel lookup map (ObjectId string -> { name, color, uid })
|
|
1188
1188
|
const channelMap = new Map();
|
|
1189
1189
|
for (const ch of channelList) {
|
|
1190
|
-
channelMap.set(ch._id.toString(), { name: ch.name, color: ch.color });
|
|
1190
|
+
channelMap.set(ch._id.toString(), { name: ch.name, color: ch.color, uid: ch.uid });
|
|
1191
1191
|
}
|
|
1192
1192
|
|
|
1193
1193
|
// Parse excluded channel IDs from query params
|
|
@@ -1223,6 +1223,7 @@ export async function timeline(request, response) {
|
|
|
1223
1223
|
if (info) {
|
|
1224
1224
|
item._channelName = info.name;
|
|
1225
1225
|
item._channelColor = info.color;
|
|
1226
|
+
item._channelUid = info.uid;
|
|
1226
1227
|
}
|
|
1227
1228
|
}
|
|
1228
1229
|
}
|
package/package.json
CHANGED
package/views/channel.njk
CHANGED
|
@@ -173,6 +173,39 @@
|
|
|
173
173
|
button.disabled = false;
|
|
174
174
|
}
|
|
175
175
|
});
|
|
176
|
+
|
|
177
|
+
// Handle save-for-later buttons
|
|
178
|
+
timeline.addEventListener('click', async (e) => {
|
|
179
|
+
const button = e.target.closest('.item-actions__save-later');
|
|
180
|
+
if (!button) return;
|
|
181
|
+
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
e.stopPropagation();
|
|
184
|
+
|
|
185
|
+
const url = button.dataset.url;
|
|
186
|
+
const title = button.dataset.title;
|
|
187
|
+
if (!url) return;
|
|
188
|
+
|
|
189
|
+
button.disabled = true;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetch('/readlater/save', {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: { 'Content-Type': 'application/json' },
|
|
195
|
+
body: JSON.stringify({ url, title: title || url, source: 'microsub' }),
|
|
196
|
+
credentials: 'same-origin'
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (response.ok) {
|
|
200
|
+
button.classList.add('item-actions__save-later--saved');
|
|
201
|
+
button.title = 'Saved';
|
|
202
|
+
} else {
|
|
203
|
+
button.disabled = false;
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
button.disabled = false;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
176
209
|
}
|
|
177
210
|
</script>
|
|
178
211
|
{% endblock %}
|
|
@@ -202,10 +202,22 @@
|
|
|
202
202
|
class="item-actions__button item-actions__mark-read"
|
|
203
203
|
data-action="mark-read"
|
|
204
204
|
data-item-id="{{ item._id }}"
|
|
205
|
+
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
|
|
205
206
|
title="Mark as read">
|
|
206
207
|
{{ icon("checkboxChecked") }}
|
|
207
208
|
<span class="visually-hidden">Mark read</span>
|
|
208
209
|
</button>
|
|
209
210
|
{% endif %}
|
|
211
|
+
{% if application.readlaterEndpoint %}
|
|
212
|
+
<button type="button"
|
|
213
|
+
class="item-actions__button item-actions__save-later"
|
|
214
|
+
data-action="save-later"
|
|
215
|
+
data-url="{{ item.url }}"
|
|
216
|
+
data-title="{{ item.name or '' }}"
|
|
217
|
+
title="Save for later">
|
|
218
|
+
{{ icon("bookmark") }}
|
|
219
|
+
<span class="visually-hidden">Save for later</span>
|
|
220
|
+
</button>
|
|
221
|
+
{% endif %}
|
|
210
222
|
</div>
|
|
211
223
|
</article>
|
package/views/timeline.njk
CHANGED
|
@@ -95,6 +95,94 @@
|
|
|
95
95
|
break;
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
// Handle individual mark-read buttons
|
|
100
|
+
const microsubApiUrl = '{{ baseUrl }}'.replace(/\/reader$/, '');
|
|
101
|
+
|
|
102
|
+
timeline.addEventListener('click', async (e) => {
|
|
103
|
+
const button = e.target.closest('.item-actions__mark-read');
|
|
104
|
+
if (!button) return;
|
|
105
|
+
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
|
|
109
|
+
const itemId = button.dataset.itemId;
|
|
110
|
+
const channelUid = button.dataset.channelUid;
|
|
111
|
+
if (!itemId || !channelUid) return;
|
|
112
|
+
|
|
113
|
+
button.disabled = true;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const formData = new URLSearchParams();
|
|
117
|
+
formData.append('action', 'timeline');
|
|
118
|
+
formData.append('method', 'mark_read');
|
|
119
|
+
formData.append('channel', channelUid);
|
|
120
|
+
formData.append('entry', itemId);
|
|
121
|
+
|
|
122
|
+
const response = await fetch(microsubApiUrl, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
125
|
+
body: formData.toString(),
|
|
126
|
+
credentials: 'same-origin'
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (response.ok) {
|
|
130
|
+
const card = button.closest('.item-card');
|
|
131
|
+
if (card) {
|
|
132
|
+
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
133
|
+
card.style.opacity = '0';
|
|
134
|
+
card.style.transform = 'translateX(-20px)';
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
const wrapper = card.closest('.timeline-view__item');
|
|
137
|
+
if (wrapper) wrapper.remove();
|
|
138
|
+
else card.remove();
|
|
139
|
+
if (timeline.querySelectorAll('.item-card').length === 0) {
|
|
140
|
+
location.reload();
|
|
141
|
+
}
|
|
142
|
+
}, 300);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
console.error('Failed to mark item as read');
|
|
146
|
+
button.disabled = false;
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Error marking item as read:', error);
|
|
150
|
+
button.disabled = false;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Handle save-for-later buttons
|
|
155
|
+
timeline.addEventListener('click', async (e) => {
|
|
156
|
+
const button = e.target.closest('.item-actions__save-later');
|
|
157
|
+
if (!button) return;
|
|
158
|
+
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
e.stopPropagation();
|
|
161
|
+
|
|
162
|
+
const url = button.dataset.url;
|
|
163
|
+
const title = button.dataset.title;
|
|
164
|
+
if (!url) return;
|
|
165
|
+
|
|
166
|
+
button.disabled = true;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetch('/readlater/save', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({ url, title: title || url, source: 'microsub' }),
|
|
173
|
+
credentials: 'same-origin'
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (response.ok) {
|
|
177
|
+
button.classList.add('item-actions__save-later--saved');
|
|
178
|
+
button.title = 'Saved';
|
|
179
|
+
} else {
|
|
180
|
+
button.disabled = false;
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
button.disabled = false;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
98
186
|
}
|
|
99
187
|
</script>
|
|
100
188
|
{% endblock %}
|