Files
indiekit-endpoint-microsub/views/channel.njk
Ricardo 4819c229cd feat: restore full microsub implementation with reader UI
Restores complete implementation from feat/endpoint-microsub branch:
- Reader UI with views (reader.njk, channel.njk, feeds.njk, etc.)
- Feed polling, parsing, and normalization
- WebSub subscriber
- SSE realtime updates
- Redis caching
- Search indexing
- Media proxy
- Webmention processing
2026-02-06 20:20:25 +01:00

103 lines
3.5 KiB
Plaintext

{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="channel">
<header class="channel__header">
<a href="{{ baseUrl }}/channels" class="back-link">
{{ icon("previous") }} {{ __("microsub.channels.title") }}
</a>
<div class="channel__actions">
<form action="{{ baseUrl }}/api/mark-read" method="POST" style="display: inline;">
<input type="hidden" name="channel" value="{{ channel.uid }}">
<input type="hidden" name="entry" value="last-read-entry">
<button type="submit" class="button button--secondary button--small">
{{ icon("checkboxChecked") }} {{ __("microsub.reader.markAllRead") }}
</button>
</form>
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--secondary button--small">
{{ icon("syndicate") }} {{ __("microsub.feeds.title") }}
</a>
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/settings" class="button button--secondary button--small">
{{ icon("updatePost") }} {{ __("microsub.channels.settings") }}
</a>
</div>
</header>
{% if items.length > 0 %}
<div class="timeline" id="timeline" data-channel="{{ channel.uid }}">
{% for item in items %}
{% include "partials/item-card.njk" %}
{% endfor %}
</div>
{% if paging %}
<nav class="timeline__paging" aria-label="Pagination">
{% if paging.before %}
<a href="?before={{ paging.before }}" class="button button--secondary">
{{ icon("previous") }} {{ __("microsub.reader.newer") }}
</a>
{% else %}
<span></span>
{% endif %}
{% if paging.after %}
<a href="?after={{ paging.after }}" class="button button--secondary">
{{ __("microsub.reader.older") }} {{ icon("next") }}
</a>
{% endif %}
</nav>
{% endif %}
{% else %}
<div class="reader__empty">
{{ icon("syndicate") }}
<p>{{ __("microsub.timeline.empty") }}</p>
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--primary">
{{ __("microsub.feeds.subscribe") }}
</a>
</div>
{% endif %}
</div>
<script type="module">
// Keyboard navigation (j/k for items, o to open)
const timeline = document.getElementById('timeline');
if (timeline) {
const items = Array.from(timeline.querySelectorAll('.item-card'));
let currentIndex = -1;
function focusItem(index) {
if (items[currentIndex]) {
items[currentIndex].classList.remove('item-card--focused');
}
currentIndex = Math.max(0, Math.min(index, items.length - 1));
if (items[currentIndex]) {
items[currentIndex].classList.add('item-card--focused');
items[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
switch(e.key) {
case 'j':
e.preventDefault();
focusItem(currentIndex + 1);
break;
case 'k':
e.preventDefault();
focusItem(currentIndex - 1);
break;
case 'o':
case 'Enter':
e.preventDefault();
if (items[currentIndex]) {
const link = items[currentIndex].querySelector('.item-card__link');
if (link) link.click();
}
break;
}
});
}
</script>
{% endblock %}