fedikirche-trends im footer der index.html und als neue Seite angelegt

This commit is contained in:
Jörg Lohrer 2025-02-05 08:34:02 +01:00
parent 4bf1d87477
commit ffc3d74453
2 changed files with 600 additions and 0 deletions

599
fedikirche-trends.html Normal file
View file

@ -0,0 +1,599 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Mastodon-Trends FediKirche</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Mastodon-Stile (optional) -->
<link rel="stylesheet" href="https://unpkg.com/mastodon-css@latest/dist/mastodon.min.css">
<!-- DOMPurify -->
<script src="https://unpkg.com/dompurify@2.4.4/dist/purify.min.js"></script>
<!-- Chart.js (nur falls Mini-Charts weiterverwendet) -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="./fonts/fonts.css?v=1.0.0">
<link rel="stylesheet" href="./style/index.css">
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f0f2f5;
}
.header-bar {
background-color: #ffffff;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-align: center;
}
.controls {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
padding: 20px;
background-color: #eaeaea;
flex-wrap: wrap;
}
.controls label, .controls select, .controls button {
font-size: 16px;
}
.controls select, .controls button {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #fff;
cursor: pointer;
}
.controls button:hover {
background-color: #f0f0f0;
}
#error-message {
color: red;
font-weight: bold;
text-align: center;
margin-top: 10px;
}
.loading {
text-align: center;
color: #555;
font-style: italic;
margin: 10px 0;
}
.column { padding: 20px; }
@media (max-width: 600px) {
.column { padding: 10px; }
}
/* Toot-Grid */
.toot-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.toot-tile {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
transition: transform 0.2s, box-shadow 0.2s;
}
.toot-tile:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.toot-header { display: flex; align-items: center; margin-bottom: 10px; }
.toot-avatar {
width: 50px; height: 50px;
border-radius: 50%; margin-right: 10px; object-fit: cover;
}
.toot-author {
font-weight: bold; font-size: 16px; color: #333;
}
.toot-content {
margin-bottom: 10px; font-size: 14px; color: #555; word-wrap: break-word;
flex-grow: 1;
}
.toot-media img { max-width: 100%; border-radius: 4px; margin-top: 10px; }
.toot-footer {
display: flex; justify-content: space-between; align-items: center;
font-size: 12px; color: #777;
}
.actions { display: flex; gap: 10px; }
.actions button {
background: none; border: none; cursor: pointer; color: #777;
display: flex; align-items: center; gap: 5px; font-size: 14px;
}
.actions button:hover { color: #000; }
.toot-timestamp { font-size: 10px; color: #999; }
/* Link-Grid */
.link-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.link-tile {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
display: flex; flex-direction: column; gap: 10px;
}
.link-tile:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.link-title a {
font-size: 16px; font-weight: bold; color: #333; text-decoration: none;
}
.link-title a:hover { text-decoration: underline; }
.link-desc { font-size: 14px; color: #555; }
.link-hits { font-size: 12px; color: #777; }
/* Mini-Chart optional */
.mini-chart {
width: 200px !important;
height: 120px !important;
margin-top: 5px;
}
</style>
</head>
<body>
<header class="header-bar">
<h1>Mastodon-Trends FediKirche</h1>
</header>
<nav>
<a href="https://codeberg.org/kirche-im-netz/Startodon-Hub" target="_blank" style="color: white; margin: 0 1rem; text-decoration: none;">🛠️&nbsp;Quellcode</a>
<a href="rechtlicheHinweise.html" style="color: white; margin: 0 1rem; text-decoration: none;">🛡️&nbsp;Impressum/Datenschutz</a>
<a href="fedikirche-trends.html" style="color: white; margin: 0 1rem; text-decoration: none;">📈&nbsp;FediKirche Trends</a>
</nav>
<div class="controls">
<label for="social-select">Plattform:</label>
<select id="social-select" aria-label="Plattform auswählen">
<option value="alle-fedikirche" selected>Trends alle FediKirche-Instanzen</option>
<option value="kirche.social">Kirche.social</option>
<option value="reliverse.social">Reliverse.social</option>
<option value="libori.social">Libori.social</option>
<option value="katholisch.social">Katholisch.social</option>
</select>
<button id="fetch-trends-button" aria-label="Hole aktuelle Trends">Trends abrufen</button>
</div>
<div id="error-message"></div>
<!-- Loading-Anzeigen -->
<div id="loading-tags" class="loading" style="display:none;">Lade Tags...</div>
<div id="loading-statuses" class="loading" style="display:none;">Lade Statuses...</div>
<div id="loading-links" class="loading" style="display:none;">Lade Links...</div>
<section class="column" aria-labelledby="trends-tags-header">
<header class="header-bar">
<h2 id="trends-tags-header">Trends: Tags</h2>
</header>
<div class="item-list">
<ul id="trends-tags" class="media-list"></ul>
</div>
</section>
<section class="column" aria-labelledby="trends-statuses-header">
<header class="header-bar">
<h2 id="trends-statuses-header">Trends: Statuses</h2>
</header>
<div class="item-list">
<div id="trends-statuses" class="toot-grid"></div>
</div>
</section>
<section class="column" aria-labelledby="trends-links-header">
<header class="header-bar">
<h2 id="trends-links-header">Trends: Links</h2>
</header>
<div id="trends-links" class="link-grid"></div>
</section>
<footer>
<p>&copy; 2025 FediKirche</p>
<div class="footer-credits">
<p>Bildnachweis:</p>
<ul style="list-style: none; padding: 0; margin: 0;">
<li>Header-Bild <a href="https://unsplash.com/de/@joephotography" target="_blank">Akira Hojo via Unsplash</a></li>
<li>Bild zum Willkommen <a href="https://unsplash.com/@battenhall" target="_blank">Battenhall via Unsplash</a></li>
</ul>
</div>
</footer>
<script>
const errorMessageEl = document.getElementById('error-message');
const loadingTagsEl = document.getElementById('loading-tags');
const loadingStatusesEl = document.getElementById('loading-statuses');
const loadingLinksEl = document.getElementById('loading-links');
// Cache-Settings
const CACHE_TTL = 60 * 1000; // 1 Minute
const API_VERSION = 'v1'; // Falls man mal wechseln muss
let isFetching = false; // Throttling-Flag
const button = document.getElementById('fetch-trends-button');
button.addEventListener('click', onFetchTrends);
function onFetchTrends() {
if (isFetching) return;
isFetching = true;
errorMessageEl.textContent = '';
const platform = document.getElementById('social-select').value;
if (platform === 'alle-fedikirche') {
fetchAllTrends();
return;
}
const baseUrl = `https://${platform}/api/${API_VERSION}/trends`;
// Paralleles Laden von [Tags, Statuses, Links]
loadingTagsEl.style.display = 'block';
loadingStatusesEl.style.display = 'block';
loadingLinksEl.style.display = 'block';
Promise.all([
fetchTags(baseUrl, `${platform}-${API_VERSION}-tags`),
fetchStatuses(baseUrl, `${platform}-${API_VERSION}-statuses`),
fetchLinks(baseUrl, `${platform}-${API_VERSION}-links`)
])
.catch(e => {
showError(e.message || e);
})
.finally(() => {
isFetching = false;
loadingTagsEl.style.display = 'none';
loadingStatusesEl.style.display = 'none';
loadingLinksEl.style.display = 'none';
});
}
// Einheitliche Fehlerbehandlung
function handleErrors(response) {
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
return response.json();
}
function showError(msg) {
errorMessageEl.textContent = msg;
}
// Cache-Funktionen
function getCachedData(key) {
const cached = localStorage.getItem(key);
if (!cached) return null;
try {
const obj = JSON.parse(cached);
if (Date.now() - obj.timestamp < CACHE_TTL) {
return obj.data;
} else {
localStorage.removeItem(key);
return null;
}
} catch (err) {
localStorage.removeItem(key);
return null;
}
}
function setCachedData(key, data) {
const obj = { timestamp: Date.now(), data };
localStorage.setItem(key, JSON.stringify(obj));
}
// DRY-Fetcher (Template)
function createFetcher(endpoint, processData, renderFn) {
return (baseUrl, cacheKey) => {
const fromCache = getCachedData(cacheKey);
if (fromCache) {
const processed = processData(fromCache);
renderFn(processed);
return Promise.resolve(processed);
}
return fetch(`${baseUrl}/${endpoint}`)
.then(handleErrors)
.then(data => {
const processed = processData(data);
setCachedData(cacheKey, processed);
renderFn(processed);
return processed;
});
};
}
// Sortier-Helfer (z.B. nach Datum)
function sortByDateDesc(a, b) {
return new Date(b.created_at) - new Date(a.created_at);
}
// Fetch-Funktionen
const fetchTags = createFetcher('', d => d, renderTags);
const fetchStatuses = createFetcher('statuses', d => d.sort(sortByDateDesc), renderStatuses);
const fetchLinks = createFetcher('links', sortLinks, renderLinks);
function sortLinks(links) {
// Falls visits_count relevant:
links.sort((a, b) => (b.visits_count || 0) - (a.visits_count || 0));
return links;
}
// Rendering
function renderTags(tags) {
const list = document.getElementById('trends-tags');
list.innerHTML = '';
tags.forEach(tag => {
const li = document.createElement('li');
li.className = "media-list__item";
// XSS-Prävention: name aus HTML entfernen
const safeName = DOMPurify.sanitize(tag.name, {ALLOWED_TAGS: []});
const link = document.createElement('a');
link.href = tag.url;
link.textContent = `#${safeName}`;
link.className = "status__content";
link.setAttribute('aria-label', `Trend Tag ${safeName}`);
li.appendChild(link);
list.appendChild(li);
});
}
function renderStatuses(statuses) {
const container = document.getElementById('trends-statuses');
container.innerHTML = '';
statuses.forEach(status => {
const tootLink = document.createElement('a');
tootLink.href = status.url;
tootLink.target = "_blank";
tootLink.rel = "noopener noreferrer";
tootLink.className = "toot-link"; // Falls Styling notwendig ist
const toot = document.createElement('div');
toot.className = "toot-tile";
tootLink.appendChild(toot); // Hier wird das toot in den Link gepackt
const header = document.createElement('div');
header.className = "toot-header";
const avatar = document.createElement('img');
avatar.className = "toot-avatar";
avatar.loading = 'lazy'; // Bild-Lazy-Loading
avatar.src = status.account?.avatar || 'https://via.placeholder.com/50';
avatar.alt = `Profilbild von ${status.account?.username || 'Unbekannt'}`;
avatar.onerror = function() { this.src = 'fallback-avatar.png'; };
const safeName = DOMPurify.sanitize(status.account?.display_name || status.account?.username || 'Unbekannt', {ALLOWED_TAGS: []});
const author = document.createElement('div');
author.className = "toot-author";
author.textContent = safeName;
header.appendChild(avatar);
header.appendChild(author);
const content = document.createElement('div');
content.className = "toot-content";
content.innerHTML = DOMPurify.sanitize(status.content || '');
if (status.media_attachments?.length) {
const mediaDiv = document.createElement('div');
mediaDiv.className = "toot-media";
status.media_attachments.forEach(media => {
if (media.type === 'image') {
const img = document.createElement('img');
img.src = media.url;
img.alt = media.description || 'Vorschaubild';
img.loading = 'lazy';
mediaDiv.appendChild(img);
}
});
content.appendChild(mediaDiv);
}
const footer = document.createElement('div');
footer.className = "toot-footer";
const timestamp = document.createElement('span');
const date = new Date(status.created_at);
timestamp.textContent = date.toLocaleString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
});
timestamp.className = "toot-timestamp";
const actions = document.createElement('div');
actions.className = "actions";
const retweetBtn = document.createElement('button');
retweetBtn.innerHTML = `🔁 ${status.reblogs_count || 0}`;
retweetBtn.setAttribute('aria-label', `${status.reblogs_count || 0} Retweets`);
const favBtn = document.createElement('button');
favBtn.innerHTML = `❤️ ${status.favourites_count || 0}`;
favBtn.setAttribute('aria-label', `${status.favourites_count || 0} Favoriten`);
actions.appendChild(retweetBtn);
actions.appendChild(favBtn);
footer.appendChild(timestamp);
footer.appendChild(actions);
toot.appendChild(header);
toot.appendChild(content);
toot.appendChild(footer);
container.appendChild(toot);
});
}
function renderLinks(links) {
const container = document.getElementById('trends-links');
container.innerHTML = '';
links.forEach((linkItem, index) => {
const tile = document.createElement('div');
tile.className = "link-tile";
const titleDiv = document.createElement('div');
titleDiv.className = 'link-title';
// XSS-Prävention
const safeTitle = DOMPurify.sanitize(linkItem.title || linkItem.url, {ALLOWED_TAGS: []});
const linkEl = document.createElement('a');
linkEl.href = linkItem.url;
linkEl.textContent = safeTitle;
linkEl.setAttribute('aria-label', `Trend Link: ${safeTitle}`);
titleDiv.appendChild(linkEl);
const descP = document.createElement('p');
descP.className = 'link-desc';
// Beschreibung ist evtl. HTML -> sanitizen
descP.innerHTML = DOMPurify.sanitize(linkItem.description || '');
const hitsSpan = document.createElement('span');
hitsSpan.className = 'link-hits';
const hitsValue = linkItem.visits_count || 0;
hitsSpan.textContent = `Aufrufe: ${hitsValue}`;
tile.appendChild(titleDiv);
tile.appendChild(descP);
tile.appendChild(hitsSpan);
// Falls Verlaufsdaten existieren -> optional Mini-Chart
if (linkItem.history && linkItem.history.length > 0) {
const chartCanvas = document.createElement('canvas');
chartCanvas.className = 'mini-chart';
chartCanvas.id = `mini-chart-${index}`;
tile.appendChild(chartCanvas);
createMiniChart(linkItem, chartCanvas);
}
container.appendChild(tile);
});
}
// Beispielhafte Mini-Chart-Funktion (optional)
function createMiniChart(linkItem, canvas) {
const labels = linkItem.history.map(h => {
const ms = parseInt(h.day, 10) * 1000;
const dt = new Date(ms);
return dt.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
});
const accountsData = linkItem.history.map(h => parseInt(h.accounts, 10) || 0);
const usesData = linkItem.history.map(h => parseInt(h.uses, 10) || 0);
new Chart(canvas, {
type: 'line',
data: {
labels,
datasets: [
{
label: 'Accounts',
data: accountsData,
borderColor: 'blue',
backgroundColor: 'rgba(0,0,255,0.1)',
fill: true,
tension: 0.2
},
{
label: 'Uses',
data: usesData,
borderColor: 'green',
backgroundColor: 'rgba(0,255,0,0.1)',
fill: true,
tension: 0.2
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { display: false }
},
scales: {
y: { beginAtZero: true }
}
}
});
}
// Neue Funktion zum Abrufen aller Trends
function fetchAllTrends() {
const platforms = [
'reliverse.social',
'libori.social',
'katholisch.social'
];
loadingTagsEl.style.display = 'block';
loadingStatusesEl.style.display = 'block';
loadingLinksEl.style.display = 'block';
errorMessageEl.textContent = '';
Promise.all(platforms.map(platform => {
const baseUrl = `https://${platform}/api/${API_VERSION}/trends`;
return Promise.all([
fetchTags(baseUrl, `${platform}-${API_VERSION}-tags`),
fetchStatuses(baseUrl, `${platform}-${API_VERSION}-statuses`),
fetchLinks(baseUrl, `${platform}-${API_VERSION}-links`)
]);
}))
.then(results => {
// Aggregiere die Tags
let allTags = [];
results.forEach(([tags, statuses, links]) => {
allTags = allTags.concat(tags);
});
// Entferne Duplikate basierend auf dem Namen
const uniqueTags = Array.from(new Map(allTags.map(tag => [tag.name, tag])).values());
renderTags(uniqueTags);
// Aggregiere die Statuses
let allStatuses = [];
results.forEach(([tags, statuses, links]) => {
allStatuses = allStatuses.concat(statuses);
});
// Sortiere nach Datum absteigend
allStatuses.sort(sortByDateDesc);
renderStatuses(allStatuses);
// Aggregiere die Links
let allLinks = [];
results.forEach(([tags, statuses, links]) => {
allLinks = allLinks.concat(links);
});
// Sortiere nach visits_count absteigend
allLinks.sort((a, b) => (b.visits_count || 0) - (a.visits_count || 0));
renderLinks(allLinks);
})
.catch(e => {
showError(e.message || e);
})
.finally(() => {
isFetching = false;
loadingTagsEl.style.display = 'none';
loadingStatusesEl.style.display = 'none';
loadingLinksEl.style.display = 'none';
});
}
</script>
</body>
</html>

View file

@ -163,6 +163,7 @@
<a href="https://codeberg.org/kirche-im-netz/Startodon-Hub" target="_blank" style="color: white; margin: 0 1rem; text-decoration: none;">🛠️&nbsp;Quellcode</a>
<a href="https://matrix.to/#/#fediverse-hub:mueller-post.de" target="_blank" style="color: white; margin: 0 1rem; text-decoration: none;">🗨️&nbsp;Entwicklungschat&nbsp;[Matrix]</a>
<a href="rechtlicheHinweise.html" style="color: white; margin: 0 1rem; text-decoration: none;">🛡️&nbsp;Impressum/Datenschutz</a>
<a href="fedikirche-trends.html" style="color: white; margin: 0 1rem; text-decoration: none;">📈&nbsp;FediKirche Trends</a>
</nav>
<div class="footer-credits">