feat: add SSO user favorites dashboard
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
This commit is contained in:
166
app.js
166
app.js
@@ -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, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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(() => {
|
||||
|
||||
Reference in New Issue
Block a user