Initial commit: RSS feed dashboard aggregating YouTube, Reddit and RT
- PHP 8 + PDO SQLite cache with on-demand TTL refresh and dedup by link hash - laminas-feed + Guzzle for RSS/Atom parsing and HTTP - Bootstrap 5 UI with source and time-range filters, AJAX load-more - User cron-free deployment under Apache/php-fpm; .htaccess hardening
This commit is contained in:
+124
@@ -0,0 +1,124 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var feed = document.getElementById('feed');
|
||||
if (!feed) {
|
||||
return;
|
||||
}
|
||||
|
||||
var loadMoreBtn = document.getElementById('load-more');
|
||||
var emptyState = document.getElementById('feed-empty');
|
||||
var spinner = loadMoreBtn ? loadMoreBtn.querySelector('.spinner-border') : null;
|
||||
|
||||
var apiUrl = feed.dataset.api;
|
||||
var source = feed.dataset.source || '';
|
||||
var range = feed.dataset.range || '';
|
||||
|
||||
var currentPage = parseInt(feed.dataset.page, 10) || 1;
|
||||
var hasMore = feed.dataset.hasMore === '1';
|
||||
var loading = false;
|
||||
|
||||
function timeAgo(iso) {
|
||||
var ts = Date.parse(iso);
|
||||
if (isNaN(ts)) {
|
||||
return '';
|
||||
}
|
||||
var diff = Math.floor((Date.now() - ts) / 1000);
|
||||
if (diff < 60) {
|
||||
return 'just now';
|
||||
}
|
||||
var units = [
|
||||
[31536000, 'y'], [2592000, 'mo'], [604800, 'w'],
|
||||
[86400, 'd'], [3600, 'h'], [60, 'm']
|
||||
];
|
||||
for (var i = 0; i < units.length; i++) {
|
||||
if (diff >= units[i][0]) {
|
||||
return Math.floor(diff / units[i][0]) + units[i][1] + ' ago';
|
||||
}
|
||||
}
|
||||
return 'just now';
|
||||
}
|
||||
|
||||
function refreshTimes() {
|
||||
document.querySelectorAll('time[data-timestamp]').forEach(function (el) {
|
||||
var text = timeAgo(el.dataset.timestamp);
|
||||
if (text) {
|
||||
el.textContent = text;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
if (!loadMoreBtn) {
|
||||
return;
|
||||
}
|
||||
loadMoreBtn.disabled = isLoading;
|
||||
if (spinner) {
|
||||
spinner.classList.toggle('d-none', !isLoading);
|
||||
}
|
||||
}
|
||||
|
||||
function updateButton() {
|
||||
if (!loadMoreBtn) {
|
||||
return;
|
||||
}
|
||||
loadMoreBtn.classList.toggle('d-none', !hasMore);
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (loading || !hasMore) {
|
||||
return;
|
||||
}
|
||||
loading = true;
|
||||
setLoading(true);
|
||||
|
||||
var params = new URLSearchParams({ page: String(currentPage + 1) });
|
||||
if (source) {
|
||||
params.set('source', source);
|
||||
}
|
||||
if (range) {
|
||||
params.set('range', range);
|
||||
}
|
||||
|
||||
fetch(apiUrl + '?' + params.toString(), {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
if (data.html) {
|
||||
feed.insertAdjacentHTML('beforeend', data.html);
|
||||
}
|
||||
|
||||
currentPage = data.page;
|
||||
hasMore = Boolean(data.has_more);
|
||||
feed.dataset.page = String(currentPage);
|
||||
feed.dataset.hasMore = hasMore ? '1' : '0';
|
||||
|
||||
if (emptyState) {
|
||||
emptyState.classList.toggle('d-none', data.total > 0);
|
||||
}
|
||||
|
||||
refreshTimes();
|
||||
updateButton();
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error('Failed to load more items:', error);
|
||||
})
|
||||
.finally(function () {
|
||||
loading = false;
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (loadMoreBtn) {
|
||||
loadMoreBtn.addEventListener('click', loadMore);
|
||||
}
|
||||
|
||||
refreshTimes();
|
||||
window.setInterval(refreshTimes, 60000);
|
||||
})();
|
||||
Reference in New Issue
Block a user