Aktueller Stand

This commit is contained in:
MDeeApp
2025-10-19 12:14:03 +02:00
parent 327a663bcf
commit 9745d38995
6 changed files with 1304 additions and 189 deletions

View File

@@ -25,7 +25,7 @@ const PROFILE_NAMES = {
4: 'Profil 4',
5: 'Profil 5'
};
function apiFetch(url, options = {}) {
const config = {
...options,
@@ -68,13 +68,26 @@ const autoRefreshToggle = document.getElementById('autoRefreshToggle');
const autoRefreshIntervalSelect = document.getElementById('autoRefreshInterval');
const sortModeSelect = document.getElementById('sortMode');
const sortDirectionToggle = document.getElementById('sortDirectionToggle');
const bookmarkPanelToggle = document.getElementById('bookmarkPanelToggle');
const bookmarkPanel = document.getElementById('bookmarkPanel');
const bookmarkPanelClose = document.getElementById('bookmarkPanelClose');
const bookmarksList = document.getElementById('bookmarksList');
const bookmarkForm = document.getElementById('bookmarkForm');
const bookmarkNameInput = document.getElementById('bookmarkName');
const bookmarkQueryInput = document.getElementById('bookmarkQuery');
const bookmarkCancelBtn = document.getElementById('bookmarkCancelBtn');
const REFRESH_SETTINGS_KEY = 'trackerRefreshSettings';
const SORT_SETTINGS_KEY = 'trackerSortSettings';
const SORT_SETTINGS_LEGACY_KEY = 'trackerSortMode';
const FACEBOOK_TRACKING_PARAMS = ['__cft__', '__tn__', '__eep__', 'mibextid'];
const FACEBOOK_TRACKING_PARAMS = ['__cft__', '__tn__', '__eep__', 'mibextid', 'set', 'comment_id', 'hoisted_section_header_type'];
const VALID_SORT_MODES = new Set(['created', 'deadline', 'smart', 'lastCheck', 'lastChange']);
const DEFAULT_SORT_SETTINGS = { mode: 'created', direction: 'desc' };
const BOOKMARKS_STORAGE_KEY = 'trackerSearchBookmarks';
const BOOKMARKS_BASE_URL = 'https://www.facebook.com/search/top';
const BOOKMARK_WINDOW_DAYS = 28;
const DEFAULT_BOOKMARKS = [];
const BOOKMARK_SUFFIXES = ['Gewinnspiel', 'gewinnen', 'verlosen'];
let autoRefreshTimer = null;
let autoRefreshSettings = {
@@ -89,6 +102,8 @@ let manualPostEditingId = null;
let manualPostModalLastFocus = null;
let manualPostModalPreviousOverflow = '';
let activeDeadlinePicker = null;
let bookmarkPanelVisible = false;
let bookmarkOutsideHandler = null;
const INITIAL_POST_LIMIT = 10;
const POST_LOAD_INCREMENT = 10;
@@ -222,6 +237,429 @@ function persistSortStorage(storage) {
}
}
function normalizeCustomBookmark(entry) {
if (!entry || typeof entry !== 'object') {
return null;
}
const query = typeof entry.query === 'string' ? entry.query.trim() : '';
if (!query) {
return null;
}
const label = typeof entry.label === 'string' && entry.label.trim() ? entry.label.trim() : query;
const id = typeof entry.id === 'string' && entry.id ? entry.id : `custom-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
return {
id,
label,
query,
type: 'custom'
};
}
function loadCustomBookmarks() {
try {
const raw = localStorage.getItem(BOOKMARKS_STORAGE_KEY);
if (!raw) {
return [];
}
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) {
return [];
}
return parsed.map(normalizeCustomBookmark).filter(Boolean);
} catch (error) {
console.warn('Konnte Bookmarks nicht laden:', error);
return [];
}
}
function saveCustomBookmarks(bookmarks) {
try {
const sanitized = Array.isArray(bookmarks)
? bookmarks.map(normalizeCustomBookmark).filter(Boolean)
: [];
localStorage.setItem(BOOKMARKS_STORAGE_KEY, JSON.stringify(sanitized));
} catch (error) {
console.warn('Konnte Bookmarks nicht speichern:', error);
}
}
function formatFacebookDateParts(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const monthLabel = `${year}-${month}`;
const dayLabel = `${year}-${month}-${day}`;
return {
year: String(year),
monthLabel,
dayLabel
};
}
function buildBookmarkFiltersParam() {
const y = String(new Date().getFullYear()); // "2025"
// start_month = Monat von (heute - BOOKMARK_WINDOW_DAYS), auf Monatsanfang (ohne Padding)
const windowAgo = new Date();
windowAgo.setDate(windowAgo.getDate() - BOOKMARK_WINDOW_DAYS);
const startMonthNum = windowAgo.getMonth() + 1; // 1..12
const startMonthLabel = `${y}-${startMonthNum}`; // z.B. "2025-9"
const startDayLabel = `${startMonthLabel}-1`; // z.B. "2025-9-1"
// Ende = Jahresende (ohne Padding), Jahre immer aktuelles Jahr als String
const endMonthLabel = `${y}-12`;
const endDayLabel = `${y}-12-31`;
// Reihenfolge wie gewünscht: top_tab zuerst, dann rp_creation_time
const filtersPayload = {
'top_tab_recent_posts:0': JSON.stringify({
name: 'top_tab_recent_posts',
args: ''
}),
'rp_creation_time:0': JSON.stringify({
name: 'creation_time',
args: JSON.stringify({
start_year: y, // als String
start_month: startMonthLabel,
end_year: y, // als String
end_month: endMonthLabel,
start_day: startDayLabel,
end_day: endDayLabel
})
})
};
const serialized = JSON.stringify(filtersPayload);
// Rohes Base64 zurückgeben (kein encodeURIComponent!)
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
return window.btoa(serialized);
} else if (typeof btoa === 'function') {
return btoa(serialized);
} else if (typeof Buffer !== 'undefined') {
return Buffer.from(serialized, 'utf8').toString('base64');
} else {
console.warn('Base64 nicht verfügbar, gebe JSON zurück (wird von URLSearchParams encodet).');
return serialized;
}
}
/*
function buildBookmarkFiltersParam() {
const y = String(new Date().getFullYear()); // "2025"
// WICHTIG: Schlüssel-Reihenfolge wie im 'soll' → top_tab zuerst
const filtersPayload = {
'top_tab_recent_posts:0': JSON.stringify({
name: 'top_tab_recent_posts',
args: ''
}),
'rp_creation_time:0': JSON.stringify({
name: 'creation_time',
args: JSON.stringify({
start_year: y, // als String
start_month: `${y}-1`, // ohne Padding
end_year: y, // als String
end_month: `${y}-12`,
start_day: `${y}-1-1`,
end_day: `${y}-12-31`
})
})
};
const serialized = JSON.stringify(filtersPayload);
// Base64 OHNE URL-Encode zurückgeben
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
return window.btoa(serialized);
} else if (typeof btoa === 'function') {
return btoa(serialized);
} else if (typeof Buffer !== 'undefined') {
return Buffer.from(serialized, 'utf8').toString('base64');
} else {
console.warn('Base64 nicht verfügbar, gebe JSON zurück (wird von URLSearchParams encodet).');
return serialized; // kein encodeURIComponent!
}
}
*/
function buildBookmarkSearchUrl(query) {
const trimmed = typeof query === 'string' ? query.trim() : '';
if (!trimmed) {
return null;
}
const searchUrl = new URL(BOOKMARKS_BASE_URL);
searchUrl.searchParams.set('q', trimmed);
searchUrl.searchParams.set('filters', buildBookmarkFiltersParam());
return searchUrl.toString();
}
function buildBookmarkSearchQueries(baseQuery) {
const trimmed = typeof baseQuery === 'string' ? baseQuery.trim() : '';
if (!trimmed) {
return [...BOOKMARK_SUFFIXES];
}
return BOOKMARK_SUFFIXES.map((suffix) => `${trimmed} ${suffix}`.trim());
}
function openBookmark(bookmark) {
if (!bookmark) {
return;
}
const queries = buildBookmarkSearchQueries(bookmark.query);
if (!queries.length) {
queries.push('');
}
queries.forEach((searchTerm) => {
const url = buildBookmarkSearchUrl(searchTerm);
if (url) {
window.open(url, '_blank', 'noopener');
}
});
}
function removeBookmark(bookmarkId) {
if (!bookmarkId) {
return;
}
const current = loadCustomBookmarks();
const next = current.filter((bookmark) => bookmark.id !== bookmarkId);
if (next.length === current.length) {
return;
}
saveCustomBookmarks(next);
renderBookmarks();
}
function renderBookmarks() {
if (!bookmarksList) {
return;
}
bookmarksList.innerHTML = '';
const items = [...DEFAULT_BOOKMARKS, ...loadCustomBookmarks()];
const staticDefault = {
id: 'default-empty',
label: 'Gewinnspiel / gewinnen / verlosen',
query: '',
type: 'default'
};
items.unshift(staticDefault);
if (!items.length) {
const empty = document.createElement('div');
empty.className = 'bookmark-empty';
empty.textContent = 'Noch keine Bookmarks vorhanden.';
empty.setAttribute('role', 'listitem');
bookmarksList.appendChild(empty);
return;
}
items.forEach((bookmark) => {
const item = document.createElement('div');
item.className = 'bookmark-item';
item.setAttribute('role', 'listitem');
const button = document.createElement('button');
button.type = 'button';
button.className = 'bookmark-button';
const label = bookmark.label || bookmark.query;
button.textContent = label;
const searchVariants = buildBookmarkSearchQueries(bookmark.query);
if (searchVariants.length) {
button.title = searchVariants.map((variant) => `${variant}`).join('\n');
} else {
button.title = `Suche nach "${bookmark.query}" (letzte 4 Wochen)`;
}
button.addEventListener('click', () => openBookmark(bookmark));
item.appendChild(button);
if (bookmark.type === 'custom') {
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'bookmark-remove-btn';
removeBtn.setAttribute('aria-label', `${bookmark.label || bookmark.query} entfernen`);
removeBtn.textContent = '×';
removeBtn.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
removeBookmark(bookmark.id);
});
item.appendChild(removeBtn);
}
bookmarksList.appendChild(item);
});
}
function resetBookmarkForm() {
if (!bookmarkForm) {
return;
}
bookmarkForm.reset();
if (bookmarkNameInput) {
bookmarkNameInput.value = '';
}
if (bookmarkQueryInput) {
bookmarkQueryInput.value = '';
}
}
function ensureBookmarkOutsideHandler() {
if (bookmarkOutsideHandler) {
return bookmarkOutsideHandler;
}
bookmarkOutsideHandler = (event) => {
if (!bookmarkPanelVisible) {
return;
}
const target = event.target;
const insidePanel = bookmarkPanel && bookmarkPanel.contains(target);
const onToggle = bookmarkPanelToggle && bookmarkPanelToggle.contains(target);
if (!insidePanel && !onToggle) {
toggleBookmarkPanel(false);
}
};
return bookmarkOutsideHandler;
}
function removeBookmarkOutsideHandler() {
if (!bookmarkOutsideHandler) {
return;
}
document.removeEventListener('mousedown', bookmarkOutsideHandler, true);
document.removeEventListener('focusin', bookmarkOutsideHandler);
}
function toggleBookmarkPanel(forceVisible) {
if (!bookmarkPanel || !bookmarkPanelToggle) {
return;
}
const shouldShow = typeof forceVisible === 'boolean' ? forceVisible : !bookmarkPanelVisible;
if (shouldShow === bookmarkPanelVisible) {
return;
}
bookmarkPanelVisible = shouldShow;
bookmarkPanel.hidden = !bookmarkPanelVisible;
bookmarkPanel.setAttribute('aria-hidden', bookmarkPanelVisible ? 'false' : 'true');
bookmarkPanelToggle.setAttribute('aria-expanded', bookmarkPanelVisible ? 'true' : 'false');
if (bookmarkPanelVisible) {
renderBookmarks();
resetBookmarkForm();
if (bookmarkQueryInput) {
window.requestAnimationFrame(() => {
bookmarkQueryInput.focus();
});
}
const handler = ensureBookmarkOutsideHandler();
window.requestAnimationFrame(() => {
document.addEventListener('mousedown', handler, true);
document.addEventListener('focusin', handler);
});
} else {
resetBookmarkForm();
removeBookmarkOutsideHandler();
if (bookmarkPanelToggle) {
bookmarkPanelToggle.focus();
}
}
}
function handleBookmarkSubmit(event) {
event.preventDefault();
if (!bookmarkForm) {
return;
}
const query = bookmarkQueryInput ? bookmarkQueryInput.value.trim() : '';
const name = bookmarkNameInput ? bookmarkNameInput.value.trim() : '';
if (!query) {
resetBookmarkForm();
if (bookmarkQueryInput) {
bookmarkQueryInput.focus();
}
return;
}
const customBookmarks = loadCustomBookmarks();
const normalizedQuery = query.toLowerCase();
const existingIndex = customBookmarks.findIndex((bookmark) => bookmark.query.toLowerCase() === normalizedQuery);
const nextBookmark = {
id: existingIndex >= 0 ? customBookmarks[existingIndex].id : `custom-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
label: name || query,
query,
type: 'custom'
};
if (existingIndex >= 0) {
customBookmarks[existingIndex] = nextBookmark;
} else {
customBookmarks.push(nextBookmark);
}
saveCustomBookmarks(customBookmarks);
renderBookmarks();
resetBookmarkForm();
}
function initializeBookmarks() {
if (!bookmarksList) {
return;
}
renderBookmarks();
if (bookmarkPanel) {
bookmarkPanel.setAttribute('aria-hidden', 'true');
}
if (bookmarkPanelToggle) {
bookmarkPanelToggle.addEventListener('click', () => {
toggleBookmarkPanel();
});
}
if (bookmarkPanelClose) {
bookmarkPanelClose.addEventListener('click', () => {
toggleBookmarkPanel(false);
});
}
if (bookmarkCancelBtn) {
bookmarkCancelBtn.addEventListener('click', () => {
resetBookmarkForm();
if (bookmarkQueryInput) {
bookmarkQueryInput.focus();
}
});
}
if (bookmarkForm) {
bookmarkForm.addEventListener('submit', handleBookmarkSubmit);
}
}
function getSortSettingsPageKey() {
try {
const path = window.location.pathname;
@@ -532,7 +970,9 @@ function normalizeFacebookPostUrl(rawValue) {
const cleanedParams = new URLSearchParams();
parsed.searchParams.forEach((paramValue, paramKey) => {
const lowerKey = paramKey.toLowerCase();
if (FACEBOOK_TRACKING_PARAMS.some((trackingKey) => lowerKey.startsWith(trackingKey))) {
const isTrackingParam = FACEBOOK_TRACKING_PARAMS.some((trackingKey) => lowerKey.startsWith(trackingKey));
const isSingleUnitParam = lowerKey === 's' && paramValue === 'single_unit';
if (isTrackingParam || isSingleUnitParam) {
return;
}
cleanedParams.append(paramKey, paramValue);
@@ -2842,6 +3282,12 @@ document.addEventListener('keydown', (event) => {
return;
}
if (bookmarkPanelVisible) {
event.preventDefault();
toggleBookmarkPanel(false);
return;
}
if (screenshotModal && screenshotModal.classList.contains('open')) {
if (screenshotModalZoomed) {
resetScreenshotZoom();
@@ -2858,6 +3304,7 @@ window.addEventListener('resize', () => {
});
// Initialize
initializeBookmarks();
loadAutoRefreshSettings();
initializeTabFromUrl();
loadSortMode();

View File

@@ -14,9 +14,36 @@
<header>
<div class="header-main">
<h1>📋 Post Tracker</h1>
<div style="display: flex; gap: 10px;">
<div class="header-links">
<a href="?view=dashboard" class="btn btn-secondary">Dashboard</a>
<a href="settings.html" class="btn btn-secondary">⚙️ Einstellungen</a>
<div class="bookmark-inline">
<button type="button" class="btn btn-secondary bookmark-inline__toggle" id="bookmarkPanelToggle" aria-expanded="false" aria-controls="bookmarkPanel">🔖 Bookmarks</button>
<div id="bookmarkPanel" class="bookmark-panel" role="dialog" aria-modal="false" hidden>
<div class="bookmark-panel__header">
<h2 class="bookmark-panel__title">🔖 Bookmarks</h2>
<button type="button" class="bookmark-panel__close" id="bookmarkPanelClose" aria-label="Schließen">×</button>
</div>
<div id="bookmarksList" class="bookmark-list" role="list" aria-live="polite"></div>
<form id="bookmarkForm" class="bookmark-form" autocomplete="off">
<div class="bookmark-form__fields">
<label class="bookmark-form__field">
<span>Titel</span>
<input type="text" id="bookmarkName" maxlength="40" placeholder="Optionaler Titel">
</label>
<label class="bookmark-form__field">
<span>Keyword *</span>
<input type="text" id="bookmarkQuery" required placeholder="z.B. gewinnspiel">
</label>
</div>
<div class="bookmark-form__actions">
<button type="submit" class="btn btn-primary">Speichern</button>
<button type="button" class="btn btn-secondary" id="bookmarkCancelBtn">Zurücksetzen</button>
</div>
<p class="bookmark-form__hint">Öffnet für das Keyword drei Suchen (… Gewinnspiel / … gewinnen / … verlosen) mit Filter auf die letzten 4 Wochen.</p>
</form>
</div>
</div>
<button type="button" class="btn btn-primary" id="openManualPostModalBtn">Beitrag hinzufügen</button>
</div>
</div>

View File

@@ -35,6 +35,13 @@ header {
gap: 12px;
}
.header-links {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.header-controls {
display: flex;
flex-wrap: wrap;
@@ -1061,6 +1068,166 @@ h1 {
cursor: not-allowed;
}
.bookmark-inline {
position: relative;
display: inline-flex;
align-items: center;
margin: 0;
}
.bookmark-inline__toggle {
padding: 8px 14px;
font-size: 14px;
}
.bookmark-panel {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: min(420px, 90vw);
background: #ffffff;
border-radius: 10px;
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.18);
padding: 16px;
z-index: 20;
border: 1px solid rgba(229, 231, 235, 0.8);
}
.bookmark-panel__header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.bookmark-panel__title {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.bookmark-panel__close {
border: none;
background: transparent;
font-size: 20px;
line-height: 1;
cursor: pointer;
color: #4b5563;
padding: 2px 6px;
}
.bookmark-panel__close:hover,
.bookmark-panel__close:focus {
color: #ef4444;
}
.bookmark-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.bookmark-item {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: #f0f2f5;
border-radius: 999px;
}
.bookmark-button {
border: none;
background: transparent;
color: #1d2129;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0;
font-size: 14px;
}
.bookmark-button:hover,
.bookmark-button:focus {
text-decoration: underline;
}
.bookmark-button:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
.bookmark-remove-btn {
border: none;
background: transparent;
color: #7f8186;
cursor: pointer;
font-size: 16px;
line-height: 1;
padding: 0 2px;
}
.bookmark-remove-btn:hover,
.bookmark-remove-btn:focus {
color: #c0392b;
}
.bookmark-form {
margin-top: 12px;
border-top: 1px solid #e4e6eb;
padding-top: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.bookmark-form__fields {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.bookmark-form__field {
display: flex;
flex-direction: column;
flex: 1 1 220px;
gap: 6px;
}
.bookmark-form__field span {
font-size: 13px;
color: #65676b;
}
.bookmark-form__field input {
border: 1px solid #d0d3d9;
border-radius: 6px;
padding: 8px 10px;
font-size: 14px;
}
.bookmark-form__actions {
display: flex;
gap: 10px;
}
.bookmark-form__hint {
margin: 0;
font-size: 12px;
color: #65676b;
}
.bookmark-empty {
font-size: 14px;
color: #65676b;
background: #f0f2f5;
border-radius: 8px;
padding: 12px;
}
.screenshot-modal {
position: fixed;
inset: 0;
@@ -1184,6 +1351,16 @@ h1 {
padding: 14px;
}
.header-main {
flex-direction: column;
align-items: flex-start;
}
.header-links {
width: 100%;
justify-content: flex-start;
}
.post-card {
padding-left: 20px;
}
@@ -1222,4 +1399,19 @@ h1 {
.btn {
width: 100%;
}
.bookmark-inline {
display: block;
margin-bottom: 10px;
}
.bookmark-panel {
position: static;
width: 100%;
margin-top: 10px;
}
.bookmark-form__actions {
flex-direction: column;
}
}