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 @@
@@ -275,6 +276,7 @@
+
diff --git a/web/style.css b/web/style.css
index cb788e0..6ce90de 100644
--- a/web/style.css
+++ b/web/style.css
@@ -4,6 +4,22 @@
box-sizing: border-box;
}
+@media (prefers-reduced-motion: reduce) {
+ .tab-btn,
+ .search-clear-btn,
+ .posts-scroll-top,
+ .post-card,
+ .auto-open-overlay,
+ .auto-open-overlay__panel {
+ transition: none;
+ }
+
+ .post-card--highlight {
+ animation: none;
+ box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.45);
+ }
+}
+
:root {
--content-max-width: 1300px;
--top-gap: 12px;
@@ -302,6 +318,27 @@ header {
align-items: center;
}
+.search-field__hint {
+ margin: 0;
+ font-size: 11px;
+ color: #64748b;
+ line-height: 1.35;
+}
+
+.search-field__hint kbd {
+ display: inline-block;
+ min-width: 18px;
+ padding: 1px 6px;
+ border-radius: 6px;
+ border: 1px solid #cbd5e1;
+ background: #f8fafc;
+ color: #0f172a;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
+ font-size: 11px;
+ font-weight: 700;
+ text-align: center;
+}
+
.search-clear-btn {
position: absolute;
right: 6px;
@@ -806,6 +843,34 @@ h1 {
gap: 6px;
}
+.posts-scroll-top {
+ position: fixed;
+ right: 20px;
+ bottom: 20px;
+ width: 42px;
+ height: 42px;
+ border: none;
+ border-radius: 999px;
+ background: #0f172a;
+ color: #fff;
+ font-size: 22px;
+ line-height: 1;
+ cursor: pointer;
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.28);
+ z-index: 45;
+ transition: transform 0.2s ease, background-color 0.2s ease;
+}
+
+.posts-scroll-top:hover {
+ background: #1d4ed8;
+ transform: translateY(-2px);
+}
+
+.posts-scroll-top:focus-visible {
+ outline: 2px solid #1d4ed8;
+ outline-offset: 2px;
+}
+
.post-card {
position: relative;
background: white;
@@ -2260,6 +2325,10 @@ h1 {
min-width: 100%;
}
+ .search-field__hint {
+ font-size: 10px;
+ }
+
.bulk-actions {
flex-direction: column;
align-items: stretch;
@@ -2455,7 +2524,15 @@ h1 {
justify-content: flex-start;
}
+ .search-filter-toggle {
+ order: 2;
+ width: 100%;
+ justify-content: flex-start;
+ }
+
.search-field {
+ order: 1;
+ width: 100%;
flex: 1 1 260px;
}
@@ -2472,6 +2549,12 @@ h1 {
.bulk-status {
width: 100%;
+ justify-content: flex-start;
+ }
+
+ .posts-scroll-top {
+ right: 14px;
+ bottom: 14px;
}
.form-actions {