feat: add SSO user favorites dashboard
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s

This commit is contained in:
Kroonk
2026-05-21 23:18:23 +02:00
parent fb540813ff
commit d40329ae7c
12 changed files with 1039 additions and 10 deletions

166
app.js
View File

@@ -16,6 +16,7 @@ const icons = {
let cards = [];
let sections = [];
let config = null;
let favorites = [];
let activeFilter = 'all';
let categoryById = new Map();
@@ -26,11 +27,25 @@ const onlineCount = document.querySelector('#onlineCount');
const serviceCount = document.querySelector('#serviceCount');
const lastChecked = document.querySelector('#lastChecked');
const dashboardSections = document.querySelector('#dashboardSections');
const favoritesSections = document.querySelector('#favoritesSections');
const loginButton = document.querySelector('#dashboardLoginButton');
const logoutButton = document.querySelector('#dashboardLogoutButton');
const favoritesLink = document.querySelector('#favoritesLink');
const userBadge = document.querySelector('#dashboardUserBadge');
function normalize(value) {
return String(value || '').toLowerCase().trim();
}
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function getLocalConfig() {
try {
const raw = window.localStorage.getItem(LOCAL_CONFIG_KEY);
@@ -181,6 +196,135 @@ function createServiceCard(service) {
return card;
}
function createFavoriteCard(favorite) {
const card = document.createElement('a');
card.href = favorite.url;
card.target = '_blank';
card.rel = 'noopener';
card.className = 'card favorite-card';
card.style.setProperty('--accent', favorite.color || '#67e8f9');
const safeLogo = favorite.logoUrl ? escapeHtml(favorite.logoUrl) : '';
const safeName = escapeHtml(favorite.name);
const safeDescription = escapeHtml(favorite.description || favorite.group || 'Favorit');
const iconSvg = icons[favorite.icon] || icons.folder;
card.innerHTML = `
<div class="card-icon favorite-icon" style="--accent: ${favorite.color || '#67e8f9'};">
${safeLogo
? `<img src="${safeLogo}" alt="" loading="lazy">`
: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${iconSvg}</svg>`}
</div>
<div class="card-content">
<span class="card-category">${escapeHtml(favorite.group || 'Favorit')}</span>
<h3>${safeName}</h3>
<p>${safeDescription}</p>
</div>
<span class="card-domain">${escapeHtml(new URL(favorite.url).hostname)}</span>
`;
return card;
}
function renderFavorites() {
if (!favoritesSections) return;
if (!MischLabsAuth.isAuthenticated()) {
favoritesSections.innerHTML = `
<section class="section favorites-section">
<div class="section-heading">
<div>
<p class="section-kicker">Persoenlich</p>
<h2>Deine Favoriten</h2>
</div>
</div>
<div class="favorites-empty">
<p>Melde dich mit SSO an, um eigene Kacheln und Weblinks zu speichern.</p>
<button class="ghost-button mini" id="favoritesLoginCallout" type="button">Mit SSO anmelden</button>
</div>
</section>
`;
document.querySelector('#favoritesLoginCallout')?.addEventListener('click', () => MischLabsAuth.login());
return;
}
if (!favorites.length) {
favoritesSections.innerHTML = `
<section class="section favorites-section">
<div class="section-heading">
<div>
<p class="section-kicker">Persoenlich</p>
<h2>Deine Favoriten</h2>
</div>
<a class="ghost-button mini" href="/favorites.html">Favoriten erstellen</a>
</div>
<div class="favorites-empty">
<p>Noch keine Favoriten. Lege eigene Kacheln fuer YouTube, Notion, Tools oder beliebige Links an.</p>
</div>
</section>
`;
return;
}
const groups = new Map();
favorites.forEach((favorite) => {
const group = favorite.group || 'Favoriten';
if (!groups.has(group)) groups.set(group, []);
groups.get(group).push(favorite);
});
favoritesSections.innerHTML = `
<section class="section favorites-section">
<div class="section-heading">
<div>
<p class="section-kicker">Persoenlich</p>
<h2>Deine Favoriten</h2>
</div>
<a class="ghost-button mini" href="/favorites.html">Bearbeiten</a>
</div>
<div class="favorite-groups"></div>
</section>
`;
const container = favoritesSections.querySelector('.favorite-groups');
[...groups.entries()].forEach(([group, items]) => {
const section = document.createElement('section');
section.className = 'favorite-group';
section.innerHTML = `
<div class="favorite-group-heading">
<h3>${escapeHtml(group)}</h3>
<span>${items.length} ${items.length === 1 ? 'Link' : 'Links'}</span>
</div>
<div class="grid favorite-grid"></div>
`;
const grid = section.querySelector('.grid');
items.forEach((favorite) => grid.appendChild(createFavoriteCard(favorite)));
container.appendChild(section);
});
}
async function loadFavorites() {
if (!MischLabsAuth.isAuthenticated()) {
favorites = [];
renderFavorites();
return;
}
try {
const response = await fetch('/api/favorites', {
cache: 'no-store',
headers: MischLabsAuth.authHeader()
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json();
favorites = Array.isArray(payload.favorites) ? payload.favorites : [];
} catch {
favorites = [];
}
renderFavorites();
}
function renderDashboard(nextConfig) {
dashboardSections.innerHTML = '';
serviceCount.textContent = String(nextConfig.services.length);
@@ -313,17 +457,37 @@ segments.forEach((segment) => {
searchInput.addEventListener('input', updateVisibility);
function updateAuthNav() {
const authenticated = MischLabsAuth.isAuthenticated();
if (loginButton) loginButton.hidden = authenticated;
if (logoutButton) logoutButton.hidden = !authenticated;
if (favoritesLink) favoritesLink.hidden = !authenticated;
if (userBadge) {
userBadge.hidden = !authenticated;
userBadge.textContent = authenticated ? MischLabsAuth.displayName() : '';
}
}
loginButton?.addEventListener('click', () => MischLabsAuth.login());
logoutButton?.addEventListener('click', () => MischLabsAuth.logout());
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(() => {});
});
}
loadConfig()
MischLabsAuth.handleCallback()
.catch((error) => {
lastChecked.textContent = error.message;
})
.then(() => loadConfig())
.then((nextConfig) => {
config = nextConfig;
updateAuthNav();
renderDashboard(config);
updateVisibility();
loadFavorites();
scheduleStatusRefresh();
})
.catch(() => {