mirror of
https://github.com/svemagie/indiekit-endpoint-microsub.git
synced 2026-04-02 15:35:00 +02:00
feat: mark source as read — split button with popover
Add ability to mark all items from a specific feed/source as read at once, instead of clicking each item individually. The mark-read button becomes a split button group with a caret that opens a popover offering "Mark [source] as read". Items without a feedId (AP items) keep the simple button. Confab-Link: http://localhost:8080/sessions/a477883d-4aef-4013-983c-ce3d3157cfba
This commit is contained in:
@@ -174,6 +174,84 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Handle caret toggle for mark-source-read popover
|
||||
timeline.addEventListener('click', (e) => {
|
||||
const caret = e.target.closest('.item-actions__mark-read-caret');
|
||||
if (!caret) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Close other open popovers
|
||||
for (const p of timeline.querySelectorAll('.item-actions__mark-read-popover:not([hidden])')) {
|
||||
if (p !== caret.nextElementSibling) p.hidden = true;
|
||||
}
|
||||
|
||||
const popover = caret.nextElementSibling;
|
||||
if (popover) popover.hidden = !popover.hidden;
|
||||
});
|
||||
|
||||
// Handle mark-source-read button
|
||||
timeline.addEventListener('click', async (e) => {
|
||||
const button = e.target.closest('.item-actions__mark-source-read');
|
||||
if (!button) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const feedId = button.dataset.feedId;
|
||||
if (!feedId) return;
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('action', 'timeline');
|
||||
formData.append('method', 'mark_read_source');
|
||||
formData.append('channel', channelUid);
|
||||
formData.append('feed', feedId);
|
||||
|
||||
const response = await fetch(microsubApiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: formData.toString(),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Animate out all cards from this feed
|
||||
const cards = timeline.querySelectorAll(`.item-card[data-feed-id="${feedId}"]`);
|
||||
for (const card of cards) {
|
||||
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
card.style.opacity = '0';
|
||||
card.style.transform = 'translateX(-20px)';
|
||||
}
|
||||
setTimeout(() => {
|
||||
for (const card of [...cards]) {
|
||||
card.remove();
|
||||
}
|
||||
if (timeline.querySelectorAll('.item-card').length === 0) {
|
||||
location.reload();
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
button.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error marking source as read:', error);
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Close popovers on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.item-actions__mark-read-group')) {
|
||||
for (const p of timeline.querySelectorAll('.item-actions__mark-read-popover:not([hidden])')) {
|
||||
p.hidden = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle save-for-later buttons
|
||||
timeline.addEventListener('click', async (e) => {
|
||||
const button = e.target.closest('.item-actions__save-later');
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#}
|
||||
<article class="item-card{% if item._is_read %} item-card--read{% endif %}"
|
||||
data-item-id="{{ item._id }}"
|
||||
data-feed-id="{{ item._feedId or '' }}"
|
||||
data-is-read="{{ item._is_read | default(false) }}">
|
||||
|
||||
{# Context bar for interactions (Aperture pattern) #}
|
||||
@@ -198,6 +199,33 @@
|
||||
<span class="visually-hidden">Bookmark</span>
|
||||
</a>
|
||||
{% if not item._is_read %}
|
||||
{% if item._feedId %}
|
||||
<span class="item-actions__mark-read-group">
|
||||
<button type="button"
|
||||
class="item-actions__button item-actions__mark-read"
|
||||
data-action="mark-read"
|
||||
data-item-id="{{ item._id }}"
|
||||
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
|
||||
{% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}
|
||||
title="Mark as read">
|
||||
{{ icon("checkboxChecked") }}
|
||||
<span class="visually-hidden">Mark read</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="item-actions__button item-actions__mark-read-caret"
|
||||
aria-label="More mark-read options"
|
||||
title="More options">▾</button>
|
||||
<div class="item-actions__mark-read-popover" hidden>
|
||||
<button type="button"
|
||||
class="item-actions__mark-source-read"
|
||||
data-feed-id="{{ item._feedId }}"
|
||||
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
|
||||
{% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}>
|
||||
Mark {{ item._source.name or item.author.name or "source" }} as read
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
{% else %}
|
||||
<button type="button"
|
||||
class="item-actions__button item-actions__mark-read"
|
||||
data-action="mark-read"
|
||||
@@ -209,6 +237,7 @@
|
||||
<span class="visually-hidden">Mark read</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if application.readlaterEndpoint %}
|
||||
<button type="button"
|
||||
class="item-actions__button item-actions__save-later"
|
||||
|
||||
@@ -152,6 +152,88 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Handle caret toggle for mark-source-read popover
|
||||
timeline.addEventListener('click', (e) => {
|
||||
const caret = e.target.closest('.item-actions__mark-read-caret');
|
||||
if (!caret) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Close other open popovers
|
||||
for (const p of timeline.querySelectorAll('.item-actions__mark-read-popover:not([hidden])')) {
|
||||
if (p !== caret.nextElementSibling) p.hidden = true;
|
||||
}
|
||||
|
||||
const popover = caret.nextElementSibling;
|
||||
if (popover) popover.hidden = !popover.hidden;
|
||||
});
|
||||
|
||||
// Handle mark-source-read button
|
||||
timeline.addEventListener('click', async (e) => {
|
||||
const button = e.target.closest('.item-actions__mark-source-read');
|
||||
if (!button) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const feedId = button.dataset.feedId;
|
||||
const channelUid = button.dataset.channelUid;
|
||||
const channelId = button.dataset.channelId;
|
||||
if (!feedId || (!channelUid && !channelId)) return;
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('action', 'timeline');
|
||||
formData.append('method', 'mark_read_source');
|
||||
formData.append('channel', channelUid || channelId);
|
||||
formData.append('feed', feedId);
|
||||
|
||||
const response = await fetch(microsubApiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: formData.toString(),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Animate out all cards from this feed
|
||||
const cards = timeline.querySelectorAll(`.item-card[data-feed-id="${feedId}"]`);
|
||||
for (const card of cards) {
|
||||
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
card.style.opacity = '0';
|
||||
card.style.transform = 'translateX(-20px)';
|
||||
}
|
||||
setTimeout(() => {
|
||||
for (const card of [...cards]) {
|
||||
const wrapper = card.closest('.timeline-view__item');
|
||||
if (wrapper) wrapper.remove();
|
||||
else card.remove();
|
||||
}
|
||||
if (timeline.querySelectorAll('.item-card').length === 0) {
|
||||
location.reload();
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
button.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error marking source as read:', error);
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Close popovers on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.item-actions__mark-read-group')) {
|
||||
for (const p of timeline.querySelectorAll('.item-actions__mark-read-popover:not([hidden])')) {
|
||||
p.hidden = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle save-for-later buttons
|
||||
timeline.addEventListener('click', async (e) => {
|
||||
const button = e.target.closest('.item-actions__save-later');
|
||||
|
||||
Reference in New Issue
Block a user