feat: add dashboard view toggle
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:48:58 +02:00
parent 4c7ef500a1
commit 9572b6116a
4 changed files with 80 additions and 12 deletions

60
app.js
View File

@@ -18,10 +18,12 @@ let sections = [];
let config = null; let config = null;
let favorites = []; let favorites = [];
let activeFilter = 'all'; let activeFilter = 'all';
let activeView = window.localStorage.getItem('mischlabs.dashboard.view') || 'all';
let categoryById = new Map(); let categoryById = new Map();
const searchInput = document.querySelector('#serviceSearch'); const searchInput = document.querySelector('#serviceSearch');
const segments = [...document.querySelectorAll('.segment')]; const segments = [...document.querySelectorAll('.segment')];
const viewOptions = [...document.querySelectorAll('.view-option')];
const emptyState = document.querySelector('#emptyState'); const emptyState = document.querySelector('#emptyState');
const onlineCount = document.querySelector('#onlineCount'); const onlineCount = document.querySelector('#onlineCount');
const serviceCount = document.querySelector('#serviceCount'); const serviceCount = document.querySelector('#serviceCount');
@@ -183,6 +185,12 @@ function createFavoriteCard(favorite) {
card.rel = 'noopener'; card.rel = 'noopener';
card.className = 'card favorite-card'; card.className = 'card favorite-card';
card.style.setProperty('--accent', favorite.color || '#67e8f9'); 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 safeLogo = favorite.logoUrl ? escapeHtml(favorite.logoUrl) : '';
const safeName = escapeHtml(favorite.name); const safeName = escapeHtml(favorite.name);
@@ -225,6 +233,7 @@ function renderFavorites() {
</section> </section>
`; `;
document.querySelector('#favoritesLoginCallout')?.addEventListener('click', () => MischLabsAuth.login()); document.querySelector('#favoritesLoginCallout')?.addEventListener('click', () => MischLabsAuth.login());
updateVisibility();
return; return;
} }
@@ -243,6 +252,7 @@ function renderFavorites() {
</div> </div>
</section> </section>
`; `;
updateVisibility();
return; return;
} }
@@ -281,6 +291,8 @@ function renderFavorites() {
items.forEach((favorite) => grid.appendChild(createFavoriteCard(favorite))); items.forEach((favorite) => grid.appendChild(createFavoriteCard(favorite)));
container.appendChild(section); container.appendChild(section);
}); });
updateVisibility();
} }
async function loadFavorites() { async function loadFavorites() {
@@ -348,22 +360,48 @@ function updateOverviewHeading(visibleCards) {
function updateVisibility() { function updateVisibility() {
const term = normalize(searchInput.value); const term = normalize(searchInput.value);
let visibleCards = 0; 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) => { cards.forEach((card) => {
const categoryMatches = activeFilter === 'all' || card.dataset.category === activeFilter; const categoryMatches = activeFilter === 'all' || card.dataset.category === activeFilter;
const searchMatches = !term || normalize(card.dataset.name).includes(term); const searchMatches = !term || normalize(card.dataset.name).includes(term);
const isVisible = categoryMatches && searchMatches; const isVisible = showServices && categoryMatches && searchMatches;
card.hidden = !isVisible; card.hidden = !isVisible;
if (isVisible) visibleCards += 1; if (isVisible) visibleCards += 1;
}); });
sections.forEach((section) => { sections.forEach((section) => {
const hasVisibleCards = [...section.querySelectorAll('.service-card')].some((card) => !card.hidden); 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); 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) { function setActiveFilter(nextFilter) {
@@ -376,6 +414,17 @@ function setActiveFilter(nextFilter) {
updateVisibility(); 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) { async function probeService(card) {
const controller = new AbortController(); const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 4500); const timeout = window.setTimeout(() => controller.abort(), 4500);
@@ -435,6 +484,10 @@ segments.forEach((segment) => {
segment.addEventListener('click', () => setActiveFilter(segment.dataset.filter)); segment.addEventListener('click', () => setActiveFilter(segment.dataset.filter));
}); });
viewOptions.forEach((option) => {
option.addEventListener('click', () => setActiveView(option.dataset.view));
});
searchInput.addEventListener('input', updateVisibility); searchInput.addEventListener('input', updateVisibility);
function updateAuthNav() { function updateAuthNav() {
@@ -466,6 +519,7 @@ MischLabsAuth.handleCallback()
config = nextConfig; config = nextConfig;
updateAuthNav(); updateAuthNav();
renderDashboard(config); renderDashboard(config);
setActiveView(activeView);
updateVisibility(); updateVisibility();
loadFavorites(); loadFavorites();
scheduleStatusRefresh(); scheduleStatusRefresh();

View File

@@ -70,6 +70,12 @@
<button class="segment" type="button" data-filter="work">Arbeit</button> <button class="segment" type="button" data-filter="work">Arbeit</button>
<button class="segment" type="button" data-filter="personal">Persoenlich</button> <button class="segment" type="button" data-filter="personal">Persoenlich</button>
</div> </div>
<div class="view-toggle" role="tablist" aria-label="Ansicht">
<button class="view-option is-active" type="button" data-view="all">Alles</button>
<button class="view-option" type="button" data-view="services">Dienste</button>
<button class="view-option" type="button" data-view="favorites">Favoriten</button>
</div>
</section> </section>
<div id="dashboardSections"></div> <div id="dashboardSections"></div>
@@ -128,6 +134,6 @@
</div> </div>
<script src="/auth.js?v=1" defer></script> <script src="/auth.js?v=1" defer></script>
<script src="/app.js?v=33" defer></script> <script src="/app.js?v=34" defer></script>
</body> </body>
</html> </html>

View File

@@ -176,7 +176,7 @@ main {
top: 0; top: 0;
z-index: 10; z-index: 10;
display: grid; display: grid;
grid-template-columns: minmax(220px, 360px) 1fr; grid-template-columns: minmax(220px, 340px) 1fr auto;
gap: 12px; gap: 12px;
align-items: center; align-items: center;
padding: 12px 0; padding: 12px 0;
@@ -214,7 +214,8 @@ main {
box-shadow: 0 0 0 3px rgba(103, 232, 249, 0.08); box-shadow: 0 0 0 3px rgba(103, 232, 249, 0.08);
} }
.segments { .segments,
.view-toggle {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 6px; gap: 6px;
@@ -222,11 +223,13 @@ main {
scrollbar-width: none; scrollbar-width: none;
} }
.segments::-webkit-scrollbar { .segments::-webkit-scrollbar,
.view-toggle::-webkit-scrollbar {
display: none; display: none;
} }
.segment { .segment,
.view-option {
min-width: max-content; min-width: max-content;
height: 36px; height: 36px;
padding: 0 13px; padding: 0 13px;
@@ -237,7 +240,8 @@ main {
cursor: pointer; cursor: pointer;
} }
.segment.is-active { .segment.is-active,
.view-option.is-active {
color: #061014; color: #061014;
background: #67e8f9; background: #67e8f9;
border-color: #67e8f9; border-color: #67e8f9;
@@ -252,7 +256,11 @@ main {
} }
.section[hidden], .section[hidden],
.service-card[hidden] { .service-card[hidden],
.favorite-card[hidden],
.favorite-group[hidden],
#dashboardSections[hidden],
#favoritesSections[hidden] {
display: none; display: none;
} }

4
sw.js
View File

@@ -1,4 +1,4 @@
const CACHE_NAME = 'mischlabs-pwa-v16'; const CACHE_NAME = 'mischlabs-pwa-v17';
const APP_SHELL = [ const APP_SHELL = [
'/', '/',
'/index.html', '/index.html',
@@ -6,7 +6,7 @@ const APP_SHELL = [
'/offline.html', '/offline.html',
'/style.css?v=28', '/style.css?v=28',
'/auth.js?v=1', '/auth.js?v=1',
'/app.js?v=33', '/app.js?v=34',
'/favorites.js?v=1', '/favorites.js?v=1',
'/services.json?v=1', '/services.json?v=1',
'/manifest.webmanifest?v=4', '/manifest.webmanifest?v=4',