feat: add save-for-later buttons to frontend pages

Add shared save-later.js module and per-item save buttons to
blogroll, podroll, listening, and news pages. Buttons are hidden
by default and only visible when logged in. Posts to the readlater
plugin API at /readlater/save.
This commit is contained in:
Ricardo
2026-02-27 16:17:16 +01:00
parent 1e900fab16
commit 4c8c44a49e
7 changed files with 178 additions and 0 deletions

View File

@@ -453,6 +453,8 @@
<script src="/js/webmentions.js?v={{ '/js/webmentions.js' | hash }}" defer></script>
{# Admin auth detection - shows dashboard link + FAB when logged in #}
<script src="/js/admin.js?v={{ '/js/admin.js' | hash }}" defer></script>
{# Save for Later buttons — active when logged in #}
<script src="/js/save-later.js?v={{ '/js/save-later.js' | hash }}" defer></script>
{# Floating Action Button - visible only when logged in #}
<div x-data="{ show: false, open: false }"

View File

@@ -176,6 +176,17 @@ permalink: /blogroll/
</svg>
Visit Blog
</a>
<button
class="save-later-btn"
:data-save-url="item.url"
:data-save-title="item.title"
data-save-source="blogroll"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
<span class="save-later-label">Save</span>
</button>
</div>
</article>
</template>

View File

@@ -741,3 +741,33 @@
display: none;
}
}
/* Save for Later buttons — hidden until auth confirmed */
.save-later-btn {
display: none;
}
body[data-indiekit-auth="true"] .save-later-btn {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
background: none;
border: 1px solid transparent;
border-radius: 6px;
padding: 2px 8px;
font-size: 0.75rem;
color: #6b7280;
transition: all 0.2s ease;
}
body[data-indiekit-auth="true"] .save-later-btn:hover {
border-color: #d1d5db;
color: #4a9eff;
}
.save-later--saved {
color: #4a9eff !important;
opacity: 0.6;
pointer-events: none;
}

51
js/save-later.js Normal file
View File

@@ -0,0 +1,51 @@
/**
* Save for Later — shared frontend module
* Handles save button clicks on blogroll, podroll, listening, and news pages.
* Only active when user is logged in (body[data-indiekit-auth="true"]).
*/
(function () {
function isLoggedIn() {
return document.body.getAttribute('data-indiekit-auth') === 'true';
}
async function saveForLater(button) {
var url = button.dataset.saveUrl;
var title = button.dataset.saveTitle || url;
var source = button.dataset.saveSource || 'manual';
if (!url) return;
button.disabled = true;
try {
var response = await fetch('/readlater/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: url, title: title, source: source }),
credentials: 'same-origin'
});
if (response.ok) {
button.classList.add('save-later--saved');
button.title = 'Saved';
button.setAttribute('aria-label', 'Saved');
var label = button.querySelector('.save-later-label');
if (label) label.textContent = 'Saved';
var icon = button.querySelector('.save-later-icon');
if (icon) icon.textContent = '🔖';
} else {
button.disabled = false;
}
} catch (e) {
button.disabled = false;
}
}
document.addEventListener('click', function (e) {
if (!isLoggedIn()) return;
var button = e.target.closest('.save-later-btn');
if (button) {
e.preventDefault();
saveForLater(button);
}
});
})();

View File

@@ -295,6 +295,16 @@ withSidebar: true
<div class="text-right flex-shrink-0">
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 rounded-full mb-1">Funkwhale</span>
<span class="text-xs text-surface-500 block">{{ listening.relativeTime }}</span>
<button
class="save-later-btn mt-1"
data-save-url="{{ listening.trackUrl }}"
data-save-title="{{ listening.track }} — {{ listening.artist }}"
data-save-source="listening"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
</button>
</div>
</div>
{% endfor %}
@@ -333,6 +343,16 @@ withSidebar: true
<div class="text-right flex-shrink-0">
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded-full mb-1">Last.fm</span>
<span class="text-xs text-surface-500 block">{{ scrobble.relativeTime }}</span>
<button
class="save-later-btn mt-1"
data-save-url="{{ scrobble.trackUrl }}"
data-save-title="{{ scrobble.track }} — {{ scrobble.artist }}"
data-save-source="listening"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
</button>
</div>
</div>
{% endfor %}
@@ -381,6 +401,16 @@ withSidebar: true
</div>
<span class="text-red-500 flex-shrink-0">&#9829;</span>
<button
class="save-later-btn flex-shrink-0"
data-save-url="{{ track.trackUrl }}"
data-save-title="{{ track.track }} — {{ track.artist }}"
data-save-source="listening"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
</button>
</div>
{% endfor %}
</div>
@@ -424,6 +454,16 @@ withSidebar: true
<p class="text-xs text-surface-500 truncate">{{ favorite.album }}</p>
{% endif %}
</div>
<button
class="save-later-btn flex-shrink-0"
data-save-url="{{ favorite.trackUrl }}"
data-save-title="{{ favorite.track }} — {{ favorite.artist }}"
data-save-source="listening"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
</button>
</div>
{% endfor %}
</div>

View File

@@ -160,6 +160,17 @@ withSidebar: true
<span class="text-primary-600 dark:text-primary-400" x-text="'#' + cat"></span>
</template>
</span>
<button
class="save-later-btn"
:data-save-url="item.link"
:data-save-title="item.title"
data-save-source="news"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
<span class="save-later-label">Save</span>
</button>
</div>
</div>
</article>
@@ -193,6 +204,17 @@ withSidebar: true
<span class="truncate max-w-[60%]" x-text="truncate(item.sourceTitle || item.feedTitle, 20)"></span>
<time :datetime="item.pubDate" x-text="formatDate(item.pubDate)"></time>
</div>
<button
class="save-later-btn mt-2"
:data-save-url="item.link"
:data-save-title="item.title"
data-save-source="news"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
<span class="save-later-label">Save</span>
</button>
</div>
</article>
</template>
@@ -249,6 +271,17 @@ withSidebar: true
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
</a>
<button
class="save-later-btn"
:data-save-url="item.link"
:data-save-title="item.title"
data-save-source="news"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
<span class="save-later-label">Save for Later</span>
</button>
<div x-show="item.categories?.length" class="flex flex-wrap gap-2">
<template x-for="cat in item.categories" :key="cat">
<span class="px-2 py-1 text-xs bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 rounded-full" x-text="cat"></span>

View File

@@ -144,6 +144,17 @@ permalink: /podroll/
</svg>
RSS
</a>
<button
class="save-later-btn"
:data-save-url="episode.url"
:data-save-title="episode.title"
data-save-source="podroll"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
<span class="save-later-label">Save</span>
</button>
</div>
</article>
</template>