From 5d3a165921f010e289ca6e21ede416a56086c66f Mon Sep 17 00:00:00 2001 From: Meik Date: Tue, 24 Feb 2026 23:09:49 +0100 Subject: [PATCH] Apply ten iterative posts UI improvements --- web/app.js | 179 +++++++++++++++++++++++++++++++++++++++++++++++-- web/index.html | 4 +- web/style.css | 83 +++++++++++++++++++++++ 3 files changed, 258 insertions(+), 8 deletions(-) diff --git a/web/app.js b/web/app.js index 7e5b481..88f9031 100644 --- a/web/app.js +++ b/web/app.js @@ -370,6 +370,7 @@ const pendingAutoOpenOverlay = document.getElementById('pendingAutoOpenOverlay') const pendingAutoOpenOverlayPanel = document.getElementById('pendingAutoOpenOverlayPanel'); const pendingAutoOpenCountdown = document.getElementById('pendingAutoOpenCountdown'); const pendingBulkStatus = document.getElementById('pendingBulkStatus'); +const postsScrollTopBtn = document.getElementById('postsScrollTopBtn'); const REFRESH_SETTINGS_KEY = 'trackerRefreshSettings'; const SORT_SETTINGS_KEY = 'trackerSortSettings'; @@ -384,12 +385,24 @@ const BOOKMARK_SUFFIXES = ['Gewinnspiel', 'gewinnen', 'verlosen']; const DEFAULT_BOOKMARK_LABEL = 'Gewinnspiel / gewinnen / verlosen'; const BOOKMARK_PREFS_KEY = 'trackerBookmarkPreferences'; const INCLUDE_EXPIRED_STORAGE_KEY = 'trackerIncludeExpired'; +const POSTS_SEARCH_STORAGE_KEY = 'trackerPostsSearchTerm'; const PENDING_BULK_COUNT_STORAGE_KEY = 'trackerPendingBulkCount'; const PENDING_AUTO_OPEN_STORAGE_KEY = 'trackerPendingAutoOpen'; const DEFAULT_PENDING_BULK_COUNT = 5; const PENDING_AUTO_OPEN_DELAY_MS = 1500; const PENDING_OPEN_COOLDOWN_STORAGE_KEY = 'trackerPendingOpenCooldown'; const PENDING_OPEN_COOLDOWN_MS = 40 * 60 * 1000; +const TAB_LABELS = { + all: 'Alle Beiträge', + pending: 'Offene Beiträge' +}; +const SORT_MODE_LABELS = { + created: 'Erstelldatum', + deadline: 'Deadline', + lastCheck: 'Letzte Teilnahme', + lastChange: 'Letzte Änderung', + smart: 'Smart (Dringlichkeit)' +}; function loadIncludeExpiredPreference() { try { @@ -527,6 +540,31 @@ function persistPendingAutoOpenEnabled(enabled) { } } +function loadPostsSearchTerm() { + try { + const stored = localStorage.getItem(POSTS_SEARCH_STORAGE_KEY); + if (typeof stored === 'string') { + return stored.slice(0, 240); + } + } catch (error) { + console.warn('Konnte Suchbegriff der Posts-Ansicht nicht laden:', error); + } + return ''; +} + +function persistPostsSearchTerm(value) { + try { + const normalized = typeof value === 'string' ? value : ''; + if (!normalized) { + localStorage.removeItem(POSTS_SEARCH_STORAGE_KEY); + return; + } + localStorage.setItem(POSTS_SEARCH_STORAGE_KEY, normalized.slice(0, 240)); + } catch (error) { + console.warn('Konnte Suchbegriff der Posts-Ansicht nicht speichern:', error); + } +} + function updateIncludeExpiredToggleUI() { if (!includeExpiredToggle) { return; @@ -535,6 +573,7 @@ function updateIncludeExpiredToggleUI() { } includeExpiredPosts = loadIncludeExpiredPreference(); +let postsSearchTerm = loadPostsSearchTerm(); let pendingBulkCount = loadPendingBulkCount(); let pendingAutoOpenEnabled = loadPendingAutoOpenEnabled(); let pendingAutoOpenTriggered = false; @@ -764,6 +803,10 @@ const tabFilteredCounts = { pending: 0, all: 0 }; +const tabDisplayCounts = { + pending: 0, + all: 0 +}; let loadMoreObserver = null; let observedLoadMoreElement = null; @@ -2019,7 +2062,28 @@ function getTabButtons() { return Array.from(document.querySelectorAll('.tab-btn[data-tab]')); } +function updateTabCountLabels(counts = {}) { + getTabButtons().forEach((button) => { + const tabKey = button.dataset.tab === 'all' ? 'all' : 'pending'; + const fallbackLabel = TAB_LABELS[tabKey] || button.textContent.trim(); + if (!button.dataset.baseLabel) { + button.dataset.baseLabel = fallbackLabel; + } + + const incomingCount = counts[tabKey]; + if (Number.isFinite(incomingCount)) { + tabDisplayCounts[tabKey] = Math.max(0, incomingCount); + } + + const count = tabDisplayCounts[tabKey]; + const label = button.dataset.baseLabel || fallbackLabel; + button.textContent = `${label} (${count})`; + button.setAttribute('aria-label', `${label} (${count})`); + }); +} + function updateTabButtons() { + updateTabCountLabels(); getTabButtons().forEach((button) => { const isActive = button.dataset.tab === currentTab; button.classList.toggle('active', isActive); @@ -2061,7 +2125,7 @@ function handleTabKeydown(event) { return; } - setTab(nextTab.dataset.tab, { updateUrl: true }); + setTab(nextTab.dataset.tab, { updateUrl: true, announceChange: true }); nextTab.focus(); } @@ -2072,6 +2136,15 @@ function updateSearchClearButtonVisibility() { const hasValue = typeof searchInput.value === 'string' && searchInput.value.trim().length > 0; searchClearBtn.hidden = !hasValue; searchClearBtn.disabled = !hasValue; + searchClearBtn.setAttribute('aria-hidden', hasValue ? 'false' : 'true'); +} + +function getSortModeLabel(mode = sortMode) { + return SORT_MODE_LABELS[mode] || SORT_MODE_LABELS.created; +} + +function getSortDirectionLabel(direction = sortDirection) { + return direction === 'asc' ? 'aufsteigend' : 'absteigend'; } function isPostsViewActive() { @@ -2079,6 +2152,27 @@ function isPostsViewActive() { return Boolean(postsView && postsView.classList.contains('app-view--active')); } +function prefersReducedMotion() { + try { + return Boolean(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); + } catch (_error) { + return false; + } +} + +function getScrollBehavior() { + return prefersReducedMotion() ? 'auto' : 'smooth'; +} + +function updatePostsScrollTopButtonVisibility() { + if (!postsScrollTopBtn) { + return; + } + const shouldShow = isPostsViewActive() && window.scrollY > 520; + postsScrollTopBtn.hidden = !shouldShow; + postsScrollTopBtn.setAttribute('aria-hidden', shouldShow ? 'false' : 'true'); +} + function updateTabInUrl() { if (!isPostsViewActive()) { return; @@ -2142,7 +2236,10 @@ function getPostListState() { const tabTotalCount = filteredItems.length; - const searchValue = searchInput && typeof searchInput.value === 'string' ? searchInput.value.trim() : ''; + const searchValueRaw = searchInput && typeof searchInput.value === 'string' + ? searchInput.value + : postsSearchTerm; + const searchValue = typeof searchValueRaw === 'string' ? searchValueRaw.trim() : ''; const searchActive = Boolean(searchValue); if (searchActive) { @@ -2349,9 +2446,9 @@ function cleanupLoadMoreObserver() { function getTabDisplayLabel(tab = currentTab) { if (tab === 'all') { - return 'Alle Beiträge'; + return TAB_LABELS.all; } - return 'Offene Beiträge'; + return TAB_LABELS.pending; } function buildPostsSummary({ @@ -2372,6 +2469,7 @@ function buildPostsSummary({ segments.push(`Tab gesamt: ${tabTotalCount}`); segments.push(`Alle Beiträge: ${totalCountAll}`); + segments.push(`Sortierung: ${getSortModeLabel()} · ${getSortDirectionLabel()}`); return `
@@ -2437,7 +2535,8 @@ function loadMorePosts(tab = currentTab, { triggeredByScroll = false } = {}) { } } -function setTab(tab, { updateUrl = true } = {}) { +function setTab(tab, { updateUrl = true, announceChange = false } = {}) { + const previousTab = currentTab; if (tab === 'all') { currentTab = 'all'; } else { @@ -2455,6 +2554,9 @@ function setTab(tab, { updateUrl = true } = {}) { } renderPosts(); maybeAutoOpenPending('tab'); + if (announceChange && previousTab !== currentTab) { + showToast(`Tab: ${getTabDisplayLabel(currentTab)}`, 'info'); + } } function initializeTabFromUrl() { @@ -3768,7 +3870,7 @@ if (pendingAutoOpenToggle) { // Tab switching getTabButtons().forEach((btn) => { btn.addEventListener('click', () => { - setTab(btn.dataset.tab, { updateUrl: true }); + setTab(btn.dataset.tab, { updateUrl: true, announceChange: true }); }); btn.addEventListener('keydown', handleTabKeydown); }); @@ -3777,6 +3879,7 @@ window.addEventListener('app:view-change', (event) => { if (event && event.detail && event.detail.view === 'posts') { updateTabInUrl(); } + updatePostsScrollTopButtonVisibility(); }); if (manualPostForm) { @@ -3832,12 +3935,35 @@ if (manualRefreshBtn) { } if (searchInput) { + if (postsSearchTerm) { + searchInput.value = postsSearchTerm; + } + searchInput.addEventListener('input', () => { + postsSearchTerm = searchInput.value || ''; + persistPostsSearchTerm(postsSearchTerm); updateSearchClearButtonVisibility(); resetVisibleCount(); renderPosts(); }); + searchInput.addEventListener('keydown', (event) => { + if (!event || event.key !== 'Escape') { + return; + } + if (typeof searchInput.value === 'string' && searchInput.value.length > 0) { + event.preventDefault(); + searchInput.value = ''; + postsSearchTerm = ''; + persistPostsSearchTerm(postsSearchTerm); + updateSearchClearButtonVisibility(); + resetVisibleCount(); + renderPosts(); + return; + } + searchInput.blur(); + }); + document.addEventListener('keydown', (event) => { if (!event || event.key !== '/' || event.metaKey || event.ctrlKey || event.altKey) { return; @@ -3860,6 +3986,8 @@ if (searchClearBtn) { return; } searchInput.value = ''; + postsSearchTerm = ''; + persistPostsSearchTerm(postsSearchTerm); updateSearchClearButtonVisibility(); resetVisibleCount(); renderPosts(); @@ -3869,6 +3997,16 @@ if (searchClearBtn) { updateSearchClearButtonVisibility(); +if (postsScrollTopBtn) { + postsScrollTopBtn.addEventListener('click', () => { + window.scrollTo({ + top: 0, + behavior: getScrollBehavior() + }); + updatePostsScrollTopButtonVisibility(); + }); +} + if (mergeModeToggle) { mergeModeToggle.addEventListener('click', () => { if (currentTab !== 'all') { @@ -4087,7 +4225,7 @@ function highlightPostCard(post) { requestAnimationFrame(() => { try { - card.scrollIntoView({ behavior: 'smooth', block: 'center' }); + card.scrollIntoView({ behavior: getScrollBehavior(), block: 'center' }); } catch (error) { console.warn('Konnte Karte nicht scrollen:', error); } @@ -4220,12 +4358,14 @@ function renderPosts() { if (container) { container.innerHTML = ''; } + updatePostsScrollTopButtonVisibility(); return; } hideError(); const container = document.getElementById('postsContainer'); if (!container) { + updatePostsScrollTopButtonVisibility(); return; } @@ -4242,6 +4382,22 @@ function renderPosts() { searchActive } = getPostListState(); + const tabCounts = sortedItems.reduce((acc, item) => { + if (!item || !item.status) { + return acc; + } + const status = item.status; + if (!status.isExpired && status.canCurrentProfileCheck && !status.isComplete) { + acc.pending += 1; + } + if (includeExpiredPosts || (!status.isExpired && !status.isComplete)) { + acc.all += 1; + } + return acc; + }, { pending: 0, all: 0 }); + updateTabCountLabels(tabCounts); + updateTabButtons(); + let filteredItems = filteredItemsResult; const focusCandidateEntry = (!focusHandled && (focusPostIdParam || focusNormalizedUrl)) ? sortedItems.find((item) => doesPostMatchFocus(item.post)) @@ -4326,6 +4482,7 @@ function renderPosts() {
`; + updatePostsScrollTopButtonVisibility(); return; } @@ -4365,6 +4522,7 @@ function renderPosts() { if (!focusHandled && focusTargetInfo.index !== -1 && focusTargetInfo.post) { requestAnimationFrame(() => highlightPostCard(focusTargetInfo.post)); } + updatePostsScrollTopButtonVisibility(); } function attachPostEventHandlers(post, status) { @@ -5337,8 +5495,13 @@ window.addEventListener('resize', () => { if (screenshotModal && screenshotModal.classList.contains('open') && !screenshotModalZoomed) { applyScreenshotModalSize(); } + updatePostsScrollTopButtonVisibility(); }); +window.addEventListener('scroll', () => { + updatePostsScrollTopButtonVisibility(); +}, { passive: true }); + window.addEventListener('app:view-change', (event) => { const view = event && event.detail ? event.detail.view : null; if (view === 'posts') { @@ -5346,6 +5509,7 @@ window.addEventListener('app:view-change', (event) => { } else { cancelPendingAutoOpen(false); } + updatePostsScrollTopButtonVisibility(); }); document.addEventListener('visibilitychange', () => { @@ -5381,6 +5545,7 @@ async function bootstrapApp() { checkAutoCheck(); startUpdatesStream(); applyAutoRefreshSettings(); + updatePostsScrollTopButtonVisibility(); } bootstrapApp(); diff --git a/web/index.html b/web/index.html index 51a5feb..bd0a434 100644 --- a/web/index.html +++ b/web/index.html @@ -232,9 +232,10 @@
- +
+

Tipp: / fokussiert die Suche, Esc leert sie.

+