diff --git a/app.js b/app.js index f9edad6..4057002 100644 --- a/app.js +++ b/app.js @@ -18,10 +18,12 @@ let sections = []; let config = null; let favorites = []; let activeFilter = 'all'; +let activeView = window.localStorage.getItem('mischlabs.dashboard.view') || 'all'; let categoryById = new Map(); const searchInput = document.querySelector('#serviceSearch'); const segments = [...document.querySelectorAll('.segment')]; +const viewOptions = [...document.querySelectorAll('.view-option')]; const emptyState = document.querySelector('#emptyState'); const onlineCount = document.querySelector('#onlineCount'); const serviceCount = document.querySelector('#serviceCount'); @@ -183,6 +185,12 @@ function createFavoriteCard(favorite) { card.rel = 'noopener'; card.className = 'card favorite-card'; card.style.setProperty('--accent', favorite.color || '#67e8f9'); + card.dataset.name = [ + favorite.name, + favorite.description, + favorite.group, + favorite.url + ].join(' '); const safeLogo = favorite.logoUrl ? escapeHtml(favorite.logoUrl) : ''; const safeName = escapeHtml(favorite.name); @@ -225,6 +233,7 @@ function renderFavorites() { `; document.querySelector('#favoritesLoginCallout')?.addEventListener('click', () => MischLabsAuth.login()); + updateVisibility(); return; } @@ -243,6 +252,7 @@ function renderFavorites() { `; + updateVisibility(); return; } @@ -281,6 +291,8 @@ function renderFavorites() { items.forEach((favorite) => grid.appendChild(createFavoriteCard(favorite))); container.appendChild(section); }); + + updateVisibility(); } async function loadFavorites() { @@ -348,22 +360,48 @@ function updateOverviewHeading(visibleCards) { function updateVisibility() { const term = normalize(searchInput.value); let visibleCards = 0; + let visibleFavorites = 0; + const showServices = activeView === 'all' || activeView === 'services'; + const showFavorites = activeView === 'all' || activeView === 'favorites'; + + if (dashboardSections) { + dashboardSections.hidden = !showServices; + } + + if (favoritesSections) { + favoritesSections.hidden = !showFavorites; + } cards.forEach((card) => { const categoryMatches = activeFilter === 'all' || card.dataset.category === activeFilter; const searchMatches = !term || normalize(card.dataset.name).includes(term); - const isVisible = categoryMatches && searchMatches; + const isVisible = showServices && categoryMatches && searchMatches; card.hidden = !isVisible; if (isVisible) visibleCards += 1; }); sections.forEach((section) => { const hasVisibleCards = [...section.querySelectorAll('.service-card')].some((card) => !card.hidden); - section.hidden = !hasVisibleCards; + section.hidden = !showServices || !hasVisibleCards; + }); + + document.querySelectorAll('.favorite-card').forEach((card) => { + const isVisible = showFavorites && (!term || normalize(card.dataset.name).includes(term)); + card.hidden = !isVisible; + if (isVisible) visibleFavorites += 1; + }); + + document.querySelectorAll('.favorite-group').forEach((group) => { + const hasVisibleCards = [...group.querySelectorAll('.favorite-card')].some((card) => !card.hidden); + group.hidden = !showFavorites || !hasVisibleCards; }); updateOverviewHeading(visibleCards); - emptyState.hidden = visibleCards !== 0; + const hasVisibleContent = visibleCards + visibleFavorites > 0; + emptyState.hidden = hasVisibleContent; + emptyState.textContent = term + ? 'Kein Eintrag passt zur Suche.' + : 'In dieser Ansicht gibt es noch keine Eintraege.'; } function setActiveFilter(nextFilter) { @@ -376,6 +414,17 @@ function setActiveFilter(nextFilter) { updateVisibility(); } +function setActiveView(nextView) { + activeView = nextView; + window.localStorage.setItem('mischlabs.dashboard.view', nextView); + viewOptions.forEach((option) => { + const isActive = option.dataset.view === nextView; + option.classList.toggle('is-active', isActive); + option.setAttribute('aria-selected', String(isActive)); + }); + updateVisibility(); +} + async function probeService(card) { const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), 4500); @@ -435,6 +484,10 @@ segments.forEach((segment) => { segment.addEventListener('click', () => setActiveFilter(segment.dataset.filter)); }); +viewOptions.forEach((option) => { + option.addEventListener('click', () => setActiveView(option.dataset.view)); +}); + searchInput.addEventListener('input', updateVisibility); function updateAuthNav() { @@ -466,6 +519,7 @@ MischLabsAuth.handleCallback() config = nextConfig; updateAuthNav(); renderDashboard(config); + setActiveView(activeView); updateVisibility(); loadFavorites(); scheduleStatusRefresh(); diff --git a/index.html b/index.html index 1becadf..ea4ef13 100644 --- a/index.html +++ b/index.html @@ -70,6 +70,12 @@ + +
+ + + +
@@ -128,6 +134,6 @@ - + diff --git a/style.css b/style.css index b05ba40..148e1c0 100644 --- a/style.css +++ b/style.css @@ -176,7 +176,7 @@ main { top: 0; z-index: 10; display: grid; - grid-template-columns: minmax(220px, 360px) 1fr; + grid-template-columns: minmax(220px, 340px) 1fr auto; gap: 12px; align-items: center; padding: 12px 0; @@ -214,7 +214,8 @@ main { box-shadow: 0 0 0 3px rgba(103, 232, 249, 0.08); } -.segments { +.segments, +.view-toggle { display: flex; justify-content: flex-end; gap: 6px; @@ -222,11 +223,13 @@ main { scrollbar-width: none; } -.segments::-webkit-scrollbar { +.segments::-webkit-scrollbar, +.view-toggle::-webkit-scrollbar { display: none; } -.segment { +.segment, +.view-option { min-width: max-content; height: 36px; padding: 0 13px; @@ -237,7 +240,8 @@ main { cursor: pointer; } -.segment.is-active { +.segment.is-active, +.view-option.is-active { color: #061014; background: #67e8f9; border-color: #67e8f9; @@ -252,7 +256,11 @@ main { } .section[hidden], -.service-card[hidden] { +.service-card[hidden], +.favorite-card[hidden], +.favorite-group[hidden], +#dashboardSections[hidden], +#favoritesSections[hidden] { display: none; } diff --git a/sw.js b/sw.js index a363149..476cf21 100644 --- a/sw.js +++ b/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'mischlabs-pwa-v16'; +const CACHE_NAME = 'mischlabs-pwa-v17'; const APP_SHELL = [ '/', '/index.html', @@ -6,7 +6,7 @@ const APP_SHELL = [ '/offline.html', '/style.css?v=28', '/auth.js?v=1', - '/app.js?v=33', + '/app.js?v=34', '/favorites.js?v=1', '/services.json?v=1', '/manifest.webmanifest?v=4',