feat: add dashboard view toggle
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:
60
app.js
60
app.js
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
20
style.css
20
style.css
@@ -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
4
sw.js
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user