Files
indiekit-endpoint-blogroll/views/blogroll-source-edit.njk
Ricardo 129dc78e09 feat: add FeedLand source type for blogroll
Adds FeedLand (feedland.com or self-hosted) as a new source type alongside
OPML and Microsub. Syncs subscriptions via FeedLand's public OPML endpoint
with optional category filtering and AJAX category discovery in the admin UI.
2026-02-17 13:54:19 +01:00

195 lines
10 KiB
Plaintext

{% extends "layouts/blogroll.njk" %}
{% block blogroll %}
<form method="post" action="{% if isNew %}{{ baseUrl }}/sources{% else %}{{ baseUrl }}/sources/{{ source._id }}{% endif %}" class="blogroll-form">
<div class="blogroll-field">
<label class="label" for="name">{{ __("blogroll.sources.form.name") }}</label>
<input class="input" type="text" id="name" name="name" value="{{ source.name if source else '' }}" required>
</div>
<div class="blogroll-field">
<label class="label" for="type">{{ __("blogroll.sources.form.type") }}</label>
<select class="select" id="type" name="type" required onchange="toggleTypeFields()">
<option value="opml_url" {% if source.type == 'opml_url' %}selected{% endif %}>OPML URL (auto-sync)</option>
<option value="opml_file" {% if source.type == 'opml_file' %}selected{% endif %}>OPML File (one-time import)</option>
{% if microsubAvailable %}
<option value="microsub" {% if source.type == 'microsub' %}selected{% endif %}>Microsub Subscriptions</option>
{% endif %}
<option value="feedland" {% if source.type == 'feedland' %}selected{% endif %}>FeedLand</option>
</select>
<span class="hint">{{ __("blogroll.sources.form.typeHint") }}</span>
</div>
<div class="blogroll-field" id="urlField">
<label class="label" for="url">{{ __("blogroll.sources.form.url") }}</label>
<input class="input" type="url" id="url" name="url" value="{{ source.url if source else '' }}" placeholder="https://...">
<span class="hint">{{ __("blogroll.sources.form.urlHint") }}</span>
</div>
<div class="blogroll-field" id="opmlContentField" style="display: none;">
<label class="label" for="opmlContent">{{ __("blogroll.sources.form.opmlContent") }}</label>
<textarea class="textarea" id="opmlContent" name="opmlContent" placeholder="<?xml version=&quot;1.0&quot;?>...">{{ source.opmlContent if source else '' }}</textarea>
<span class="hint">{{ __("blogroll.sources.form.opmlContentHint") }}</span>
</div>
<div class="blogroll-field" id="microsubChannelField" style="display: none;">
<label class="label" for="channelFilter">{{ __("blogroll.sources.form.microsubChannel") | default("Microsub Channel") }}</label>
<select class="select" id="channelFilter" name="channelFilter">
<option value="">All channels</option>
{% for channel in microsubChannels %}
<option value="{{ channel.uid }}" {% if source.channelFilter == channel.uid %}selected{% endif %}>{{ channel.name }}</option>
{% endfor %}
</select>
<span class="hint">{{ __("blogroll.sources.form.microsubChannelHint") | default("Sync feeds from a specific channel, or all channels") }}</span>
</div>
<div class="blogroll-field" id="categoryPrefixField" style="display: none;">
<label class="label" for="categoryPrefix">{{ __("blogroll.sources.form.categoryPrefix") | default("Category Prefix") }}</label>
<input class="input" type="text" id="categoryPrefix" name="categoryPrefix" value="{{ source.categoryPrefix if source else '' }}" placeholder="e.g., Microsub: ">
<span class="hint">{{ __("blogroll.sources.form.categoryPrefixHint") | default("Optional prefix for blog categories (e.g., 'Following: ')") }}</span>
</div>
<div class="blogroll-field" id="feedlandInstanceField" style="display: none;">
<label class="label" for="feedlandInstance">{{ __("blogroll.sources.form.feedlandInstance") | default("FeedLand Instance URL") }}</label>
<input class="input" type="url" id="feedlandInstance" name="feedlandInstance" value="{{ source.feedlandInstance if source else 'https://feedland.com' }}" placeholder="https://feedland.com">
<span class="hint">{{ __("blogroll.sources.form.feedlandInstanceHint") | default("FeedLand instance URL (feedland.com or self-hosted)") }}</span>
</div>
<div class="blogroll-field" id="feedlandUsernameField" style="display: none;">
<label class="label" for="feedlandUsername">{{ __("blogroll.sources.form.feedlandUsername") | default("FeedLand Username") }}</label>
<input class="input" type="text" id="feedlandUsername" name="feedlandUsername" value="{{ source.feedlandUsername if source else '' }}" placeholder="e.g., davewiner">
<span class="hint">{{ __("blogroll.sources.form.feedlandUsernameHint") | default("Your FeedLand screen name") }}</span>
</div>
<div class="blogroll-field" id="feedlandCategoryField" style="display: none;">
<label class="label" for="feedlandCategory">{{ __("blogroll.sources.form.feedlandCategory") | default("FeedLand Category") }}</label>
<div style="display: flex; gap: 0.5rem; align-items: flex-start;">
<select class="select" id="feedlandCategory" name="feedlandCategory" style="flex: 1;">
<option value="">{{ __("blogroll.sources.form.feedlandCategoryAll") | default("All subscriptions") }}</option>
{% if source.feedlandCategory %}
<option value="{{ source.feedlandCategory }}" selected>{{ source.feedlandCategory }}</option>
{% endif %}
</select>
<button type="button" class="button button--secondary" onclick="loadFeedlandCategories()" id="feedlandLoadBtn">{{ __("blogroll.sources.form.feedlandLoadCategories") | default("Load") }}</button>
</div>
<span class="hint" id="feedlandCategoryHint">{{ __("blogroll.sources.form.feedlandCategoryHint") | default("Optional: sync only feeds from a specific category") }}</span>
</div>
<div class="blogroll-field">
<label class="label" for="syncInterval">{{ __("blogroll.sources.form.syncInterval") }}</label>
<select class="select" id="syncInterval" name="syncInterval">
<option value="30" {% if source.syncInterval == 30 %}selected{% endif %}>30 minutes</option>
<option value="60" {% if not source or source.syncInterval == 60 %}selected{% endif %}>1 hour</option>
<option value="180" {% if source.syncInterval == 180 %}selected{% endif %}>3 hours</option>
<option value="360" {% if source.syncInterval == 360 %}selected{% endif %}>6 hours</option>
<option value="720" {% if source.syncInterval == 720 %}selected{% endif %}>12 hours</option>
<option value="1440" {% if source.syncInterval == 1440 %}selected{% endif %}>24 hours</option>
</select>
</div>
<div class="blogroll-field blogroll-field--inline">
<input type="checkbox" id="enabled" name="enabled" {% if not source or source.enabled %}checked{% endif %}>
<label for="enabled">{{ __("blogroll.sources.form.enabled") }}</label>
</div>
<div class="blogroll-actions">
{{ button({ type: "submit", text: __("blogroll.sources.create") if isNew else __("blogroll.sources.save") }) }}
{{ button({ href: baseUrl + "/sources", text: __("blogroll.cancel"), classes: "button--secondary" }) }}
</div>
</form>
<script>
function toggleTypeFields() {
const type = document.getElementById('type').value;
const urlField = document.getElementById('urlField');
const opmlContentField = document.getElementById('opmlContentField');
const microsubChannelField = document.getElementById('microsubChannelField');
const categoryPrefixField = document.getElementById('categoryPrefixField');
const feedlandInstanceField = document.getElementById('feedlandInstanceField');
const feedlandUsernameField = document.getElementById('feedlandUsernameField');
const feedlandCategoryField = document.getElementById('feedlandCategoryField');
// Hide all type-specific fields first
urlField.style.display = 'none';
opmlContentField.style.display = 'none';
if (microsubChannelField) microsubChannelField.style.display = 'none';
if (categoryPrefixField) categoryPrefixField.style.display = 'none';
if (feedlandInstanceField) feedlandInstanceField.style.display = 'none';
if (feedlandUsernameField) feedlandUsernameField.style.display = 'none';
if (feedlandCategoryField) feedlandCategoryField.style.display = 'none';
// Show fields based on type
if (type === 'opml_url') {
urlField.style.display = 'flex';
} else if (type === 'opml_file') {
opmlContentField.style.display = 'flex';
} else if (type === 'microsub') {
if (microsubChannelField) microsubChannelField.style.display = 'flex';
if (categoryPrefixField) categoryPrefixField.style.display = 'flex';
} else if (type === 'feedland') {
if (feedlandInstanceField) feedlandInstanceField.style.display = 'flex';
if (feedlandUsernameField) feedlandUsernameField.style.display = 'flex';
if (feedlandCategoryField) feedlandCategoryField.style.display = 'flex';
}
}
function loadFeedlandCategories() {
const instance = document.getElementById('feedlandInstance').value;
const username = document.getElementById('feedlandUsername').value;
const select = document.getElementById('feedlandCategory');
const btn = document.getElementById('feedlandLoadBtn');
const hint = document.getElementById('feedlandCategoryHint');
const currentValue = select.value;
if (!instance || !username) {
hint.textContent = 'Please enter instance URL and username first';
return;
}
btn.disabled = true;
btn.textContent = '...';
hint.textContent = 'Loading categories...';
const baseUrl = '{{ baseUrl }}';
const url = baseUrl + '/api/feedland-categories?instance=' + encodeURIComponent(instance) + '&username=' + encodeURIComponent(username);
fetch(url, { credentials: 'same-origin' })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) {
hint.textContent = 'Error: ' + data.error;
return;
}
// Clear existing options and rebuild safely using DOM methods
while (select.options.length > 0) select.remove(0);
var allOpt = document.createElement('option');
allOpt.value = '';
allOpt.textContent = '{{ __("blogroll.sources.form.feedlandCategoryAll") | default("All subscriptions") }}';
select.appendChild(allOpt);
var cats = data.categories || [];
cats.forEach(function(cat) {
var opt = document.createElement('option');
opt.value = cat;
opt.textContent = cat;
if (cat === currentValue) opt.selected = true;
select.appendChild(opt);
});
hint.textContent = cats.length + ' categories found' + (data.screenname ? ' for ' + data.screenname : '');
})
.catch(function(err) {
hint.textContent = 'Failed to load: ' + err.message;
})
.finally(function() {
btn.disabled = false;
btn.textContent = '{{ __("blogroll.sources.form.feedlandLoadCategories") | default("Load") }}';
});
}
// Initialize on load
toggleTypeFields();
</script>
{% endblock %}