3524 lines
101 KiB
JavaScript
3524 lines
101 KiB
JavaScript
const API_URL = 'https://fb.srv.medeba-media.de/api';
|
||
|
||
let initialViewParam = null;
|
||
|
||
// Normalize incoming routing parameters without leaving the index view
|
||
(function normalizeViewRouting() {
|
||
try {
|
||
const params = new URLSearchParams(window.location.search);
|
||
const view = params.get('view');
|
||
if (!view) {
|
||
return;
|
||
}
|
||
|
||
if (view === 'dashboard') {
|
||
initialViewParam = view;
|
||
params.delete('view');
|
||
const remaining = params.toString();
|
||
const newUrl = `${window.location.pathname}${remaining ? `?${remaining}` : ''}${window.location.hash}`;
|
||
window.history.replaceState({}, document.title, newUrl);
|
||
}
|
||
} catch (error) {
|
||
console.warn('Konnte view-Parameter nicht verarbeiten:', error);
|
||
}
|
||
})();
|
||
|
||
let focusPostIdParam = null;
|
||
let focusPostUrlParam = null;
|
||
let focusNormalizedUrl = '';
|
||
let focusHandled = false;
|
||
let initialTabOverride = null;
|
||
let focusTabAdjusted = null;
|
||
|
||
let currentProfile = 1;
|
||
let currentTab = 'pending';
|
||
let posts = [];
|
||
let profilePollTimer = null;
|
||
|
||
const MAX_PROFILES = 5;
|
||
const PROFILE_NAMES = {
|
||
1: 'Profil 1',
|
||
2: 'Profil 2',
|
||
3: 'Profil 3',
|
||
4: 'Profil 4',
|
||
5: 'Profil 5'
|
||
};
|
||
|
||
function apiFetch(url, options = {}) {
|
||
const config = {
|
||
...options,
|
||
credentials: 'include'
|
||
};
|
||
|
||
if (options && options.headers) {
|
||
config.headers = { ...options.headers };
|
||
}
|
||
|
||
return fetch(url, config);
|
||
}
|
||
|
||
const screenshotModal = document.getElementById('screenshotModal');
|
||
const screenshotModalContent = document.getElementById('screenshotModalContent');
|
||
const screenshotModalImage = document.getElementById('screenshotModalImage');
|
||
const screenshotModalClose = document.getElementById('screenshotModalClose');
|
||
const screenshotModalBackdrop = document.getElementById('screenshotModalBackdrop');
|
||
let screenshotModalLastFocus = null;
|
||
let screenshotModalPreviousOverflow = '';
|
||
let screenshotModalZoomed = false;
|
||
|
||
const manualPostForm = document.getElementById('manualPostForm');
|
||
const manualPostUrlInput = document.getElementById('manualPostUrl');
|
||
const manualPostTitleInput = document.getElementById('manualPostTitle');
|
||
const manualPostTargetSelect = document.getElementById('manualPostTarget');
|
||
const manualPostCreatorInput = document.getElementById('manualPostCreatorName');
|
||
const manualPostDeadlineInput = document.getElementById('manualPostDeadline');
|
||
const manualPostResetButton = document.getElementById('manualPostReset');
|
||
const manualPostMessage = document.getElementById('manualPostMessage');
|
||
const manualPostSubmitButton = document.getElementById('manualPostSubmitBtn');
|
||
const manualPostModal = document.getElementById('manualPostModal');
|
||
const manualPostModalContent = document.getElementById('manualPostModalContent');
|
||
const manualPostModalBackdrop = document.getElementById('manualPostModalBackdrop');
|
||
const manualPostModalClose = document.getElementById('manualPostModalClose');
|
||
const manualPostModalTitle = document.getElementById('manualPostModalTitle');
|
||
const openManualPostModalBtn = document.getElementById('openManualPostModalBtn');
|
||
|
||
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', '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'];
|
||
|
||
function initializeFocusParams() {
|
||
try {
|
||
const params = new URLSearchParams(window.location.search);
|
||
const postIdParam = params.get('postId');
|
||
const postUrlParam = params.get('postUrl');
|
||
|
||
if (postIdParam && postIdParam.trim()) {
|
||
focusPostIdParam = postIdParam.trim();
|
||
initialTabOverride = initialTabOverride || 'all';
|
||
}
|
||
|
||
if (postUrlParam && postUrlParam.trim()) {
|
||
focusPostUrlParam = postUrlParam.trim();
|
||
const normalized = normalizeFacebookPostUrl(focusPostUrlParam);
|
||
focusNormalizedUrl = normalized || focusPostUrlParam;
|
||
initialTabOverride = initialTabOverride || 'all';
|
||
}
|
||
focusHandled = false;
|
||
focusTabAdjusted = null;
|
||
} catch (error) {
|
||
console.warn('Konnte Fokus-Parameter nicht verarbeiten:', error);
|
||
}
|
||
}
|
||
|
||
let autoRefreshTimer = null;
|
||
let autoRefreshSettings = {
|
||
enabled: true,
|
||
interval: 30000
|
||
};
|
||
let sortMode = DEFAULT_SORT_SETTINGS.mode;
|
||
let sortDirection = DEFAULT_SORT_SETTINGS.direction;
|
||
let isFetchingPosts = false;
|
||
let manualPostMode = 'create';
|
||
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;
|
||
const tabVisibleCounts = {
|
||
pending: INITIAL_POST_LIMIT,
|
||
expired: INITIAL_POST_LIMIT,
|
||
all: INITIAL_POST_LIMIT
|
||
};
|
||
const tabFilteredCounts = {
|
||
pending: 0,
|
||
expired: 0,
|
||
all: 0
|
||
};
|
||
let loadMoreObserver = null;
|
||
let observedLoadMoreElement = null;
|
||
|
||
function getProfileName(profileNumber) {
|
||
if (!profileNumber) {
|
||
return '';
|
||
}
|
||
return PROFILE_NAMES[profileNumber] || `Profil ${profileNumber}`;
|
||
}
|
||
|
||
function formatDateTime(value) {
|
||
if (!value) {
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
const date = value instanceof Date ? value : new Date(value);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return '';
|
||
}
|
||
return date.toLocaleString('de-DE', { timeZone: 'Europe/Berlin' });
|
||
} catch (error) {
|
||
console.warn('Ungültiges Datum:', error);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
function formatDeadline(value) {
|
||
if (!value) {
|
||
return 'Keine Deadline';
|
||
}
|
||
|
||
try {
|
||
const date = value instanceof Date ? value : new Date(value);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return 'Ungültige Deadline';
|
||
}
|
||
return date.toLocaleString('de-DE', { timeZone: 'Europe/Berlin' });
|
||
} catch (error) {
|
||
console.warn('Ungültige Deadline:', error);
|
||
return 'Ungültige Deadline';
|
||
}
|
||
}
|
||
|
||
function formatUrlForDisplay(url) {
|
||
if (!url) {
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
const parsed = new URL(url);
|
||
const pathname = parsed.pathname === '/' ? '' : parsed.pathname;
|
||
const search = parsed.search || '';
|
||
return `${parsed.hostname}${pathname}${search}`;
|
||
} catch (error) {
|
||
return url;
|
||
}
|
||
}
|
||
|
||
function toTimestamp(value, fallback = null) {
|
||
if (!value) {
|
||
return fallback;
|
||
}
|
||
|
||
const timestamp = value instanceof Date ? value.getTime() : new Date(value).getTime();
|
||
if (Number.isNaN(timestamp)) {
|
||
return fallback;
|
||
}
|
||
|
||
return timestamp;
|
||
}
|
||
|
||
function getSortTabKey(tab = currentTab) {
|
||
if (tab === 'expired') {
|
||
return 'expired';
|
||
}
|
||
if (tab === 'all') {
|
||
return 'all';
|
||
}
|
||
return 'pending';
|
||
}
|
||
|
||
function normalizeSortMode(value) {
|
||
if (VALID_SORT_MODES.has(value)) {
|
||
return value;
|
||
}
|
||
return DEFAULT_SORT_SETTINGS.mode;
|
||
}
|
||
|
||
function normalizeSortDirection(value) {
|
||
return value === 'asc' ? 'asc' : DEFAULT_SORT_SETTINGS.direction;
|
||
}
|
||
|
||
function getSortStorage() {
|
||
try {
|
||
const raw = localStorage.getItem(SORT_SETTINGS_KEY) || localStorage.getItem(SORT_SETTINGS_LEGACY_KEY);
|
||
if (!raw) {
|
||
return {};
|
||
}
|
||
const parsed = JSON.parse(raw);
|
||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||
return parsed;
|
||
}
|
||
} catch (error) {
|
||
console.warn('Konnte Sortierdaten nicht parsen:', error);
|
||
}
|
||
return {};
|
||
}
|
||
|
||
function persistSortStorage(storage) {
|
||
try {
|
||
localStorage.setItem(SORT_SETTINGS_KEY, JSON.stringify(storage));
|
||
if (SORT_SETTINGS_KEY !== SORT_SETTINGS_LEGACY_KEY) {
|
||
localStorage.removeItem(SORT_SETTINGS_LEGACY_KEY);
|
||
}
|
||
} catch (error) {
|
||
console.warn('Konnte Sortierdaten nicht speichern:', error);
|
||
}
|
||
}
|
||
|
||
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;
|
||
if (typeof path === 'string' && path) {
|
||
return path;
|
||
}
|
||
} catch (error) {
|
||
// ignore and fall back
|
||
}
|
||
return 'default';
|
||
}
|
||
|
||
function updateSortDirectionToggleUI() {
|
||
if (!sortDirectionToggle) {
|
||
return;
|
||
}
|
||
|
||
const isAsc = sortDirection === 'asc';
|
||
sortDirectionToggle.setAttribute('aria-pressed', isAsc ? 'true' : 'false');
|
||
sortDirectionToggle.setAttribute('aria-label', isAsc ? 'Aufsteigend' : 'Absteigend');
|
||
sortDirectionToggle.title = isAsc ? 'Aufsteigend' : 'Absteigend';
|
||
sortDirectionToggle.dataset.direction = sortDirection;
|
||
|
||
const icon = sortDirectionToggle.querySelector('.sort-direction-toggle__icon');
|
||
if (icon) {
|
||
icon.textContent = isAsc ? '▲' : '▼';
|
||
} else {
|
||
sortDirectionToggle.textContent = isAsc ? '▲' : '▼';
|
||
}
|
||
}
|
||
|
||
function normalizeRequiredProfiles(post) {
|
||
if (Array.isArray(post.required_profiles) && post.required_profiles.length) {
|
||
return post.required_profiles
|
||
.map((value) => {
|
||
const parsed = parseInt(value, 10);
|
||
if (Number.isNaN(parsed)) {
|
||
return null;
|
||
}
|
||
return Math.min(MAX_PROFILES, Math.max(1, parsed));
|
||
})
|
||
.filter(Boolean);
|
||
}
|
||
|
||
const parsedTarget = parseInt(post.target_count, 10);
|
||
const count = Number.isNaN(parsedTarget)
|
||
? 1
|
||
: Math.min(MAX_PROFILES, Math.max(1, parsedTarget));
|
||
|
||
return Array.from({ length: count }, (_, index) => index + 1);
|
||
}
|
||
|
||
function updateTabButtons() {
|
||
document.querySelectorAll('.tab-btn').forEach((button) => {
|
||
if (!button.dataset.tab) {
|
||
return;
|
||
}
|
||
button.classList.toggle('active', button.dataset.tab === currentTab);
|
||
});
|
||
}
|
||
|
||
function updateTabInUrl() {
|
||
const url = new URL(window.location.href);
|
||
if (currentTab === 'pending') {
|
||
url.searchParams.set('tab', 'pending');
|
||
} else if (currentTab === 'expired') {
|
||
url.searchParams.set('tab', 'expired');
|
||
} else {
|
||
url.searchParams.set('tab', 'all');
|
||
}
|
||
window.history.replaceState({}, document.title, `${url.pathname}?${url.searchParams.toString()}${url.hash}`);
|
||
}
|
||
|
||
function getTabKey(tab = currentTab) {
|
||
if (tab === 'expired') {
|
||
return 'expired';
|
||
}
|
||
if (tab === 'all') {
|
||
return 'all';
|
||
}
|
||
return 'pending';
|
||
}
|
||
|
||
function getVisibleCount(tab = currentTab) {
|
||
const key = getTabKey(tab);
|
||
const value = tabVisibleCounts[key];
|
||
const normalized = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : INITIAL_POST_LIMIT;
|
||
return Math.max(INITIAL_POST_LIMIT, normalized);
|
||
}
|
||
|
||
function setVisibleCount(tab, count) {
|
||
const key = getTabKey(tab);
|
||
const normalized = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : INITIAL_POST_LIMIT;
|
||
tabVisibleCounts[key] = Math.max(INITIAL_POST_LIMIT, normalized);
|
||
}
|
||
|
||
function resetVisibleCount(tab = currentTab) {
|
||
setVisibleCount(tab, INITIAL_POST_LIMIT);
|
||
}
|
||
|
||
function updateFilteredCount(tab, count) {
|
||
const key = getTabKey(tab);
|
||
tabFilteredCounts[key] = Math.max(0, count || 0);
|
||
}
|
||
|
||
function cleanupLoadMoreObserver() {
|
||
if (loadMoreObserver && observedLoadMoreElement) {
|
||
loadMoreObserver.unobserve(observedLoadMoreElement);
|
||
observedLoadMoreElement = null;
|
||
}
|
||
}
|
||
|
||
function getTabDisplayLabel(tab = currentTab) {
|
||
if (tab === 'expired') {
|
||
return 'Abgelaufen/Abgeschlossen';
|
||
}
|
||
if (tab === 'all') {
|
||
return 'Alle Beiträge';
|
||
}
|
||
return 'Offene Beiträge';
|
||
}
|
||
|
||
function buildPostsSummary({
|
||
tab,
|
||
visibleCount,
|
||
filteredCount,
|
||
tabTotalCount,
|
||
totalCountAll,
|
||
searchActive
|
||
}) {
|
||
const label = getTabDisplayLabel(tab);
|
||
const segments = [];
|
||
segments.push(`<span class="posts-summary__item">Angezeigt: <strong>${visibleCount}</strong> von ${filteredCount}</span>`);
|
||
|
||
if (searchActive) {
|
||
segments.push(`<span class="posts-summary__item">Treffer: <strong>${filteredCount}</strong> von ${tabTotalCount}</span>`);
|
||
}
|
||
|
||
segments.push(`<span class="posts-summary__item">Tab gesamt: <strong>${tabTotalCount}</strong></span>`);
|
||
segments.push(`<span class="posts-summary__item">Alle Beiträge: <strong>${totalCountAll}</strong></span>`);
|
||
|
||
return `
|
||
<div class="posts-summary" role="status" aria-live="polite">
|
||
<span class="posts-summary__label">${label}</span>
|
||
${segments.join('<span class="posts-summary__separator">·</span>')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function ensureLoadMoreObserver() {
|
||
if (loadMoreObserver) {
|
||
return loadMoreObserver;
|
||
}
|
||
|
||
loadMoreObserver = new IntersectionObserver((entries) => {
|
||
entries.forEach((entry) => {
|
||
if (!entry.isIntersecting) {
|
||
return;
|
||
}
|
||
|
||
const element = entry.target;
|
||
const tab = element && element.dataset ? element.dataset.tab : currentTab;
|
||
|
||
if (loadMoreObserver) {
|
||
loadMoreObserver.unobserve(element);
|
||
}
|
||
|
||
loadMorePosts(tab, { triggeredByScroll: true });
|
||
});
|
||
}, {
|
||
root: null,
|
||
rootMargin: '200px 0px',
|
||
threshold: 0.1
|
||
});
|
||
|
||
return loadMoreObserver;
|
||
}
|
||
|
||
function observeLoadMoreElement(element, tab) {
|
||
if (!element) {
|
||
return;
|
||
}
|
||
const observer = ensureLoadMoreObserver();
|
||
observedLoadMoreElement = element;
|
||
element.dataset.tab = getTabKey(tab);
|
||
observer.observe(element);
|
||
}
|
||
|
||
function loadMorePosts(tab = currentTab, { triggeredByScroll = false } = {}) {
|
||
const key = getTabKey(tab);
|
||
const total = tabFilteredCounts[key] || 0;
|
||
const currentLimit = getVisibleCount(tab);
|
||
|
||
if (currentLimit >= total) {
|
||
return;
|
||
}
|
||
|
||
const newLimit = Math.min(total, currentLimit + POST_LOAD_INCREMENT);
|
||
setVisibleCount(tab, newLimit);
|
||
|
||
if (tab === currentTab) {
|
||
renderPosts();
|
||
}
|
||
}
|
||
|
||
function setTab(tab, { updateUrl = true } = {}) {
|
||
if (tab === 'all') {
|
||
currentTab = 'all';
|
||
} else if (tab === 'expired') {
|
||
currentTab = 'expired';
|
||
} else {
|
||
currentTab = 'pending';
|
||
}
|
||
updateTabButtons();
|
||
loadSortMode({ fromTabChange: true });
|
||
if (updateUrl) {
|
||
updateTabInUrl();
|
||
}
|
||
renderPosts();
|
||
}
|
||
|
||
function initializeTabFromUrl() {
|
||
let tabResolved = false;
|
||
try {
|
||
const params = new URLSearchParams(window.location.search);
|
||
const tabParam = params.get('tab');
|
||
if (tabParam === 'all' || tabParam === 'pending' || tabParam === 'expired') {
|
||
currentTab = tabParam;
|
||
tabResolved = true;
|
||
} else if (initialTabOverride) {
|
||
currentTab = initialTabOverride;
|
||
tabResolved = true;
|
||
} else if (initialViewParam === 'dashboard') {
|
||
currentTab = 'pending';
|
||
tabResolved = true;
|
||
}
|
||
} catch (error) {
|
||
console.warn('Konnte Tab-Parameter nicht auslesen:', error);
|
||
}
|
||
|
||
updateTabButtons();
|
||
if (tabResolved) {
|
||
updateTabInUrl();
|
||
}
|
||
}
|
||
|
||
function normalizeDeadlineInput(value) {
|
||
if (!value) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return null;
|
||
}
|
||
return date.toISOString();
|
||
} catch (error) {
|
||
console.warn('Ungültige Deadline-Eingabe:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function toDateTimeLocalValue(value) {
|
||
if (!value) {
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return '';
|
||
}
|
||
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
|
||
return local.toISOString().slice(0, 16);
|
||
} catch (error) {
|
||
console.warn('Kann Deadline nicht für Eingabe formatieren:', error);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
function normalizeFacebookPostUrl(rawValue) {
|
||
if (typeof rawValue !== 'string') {
|
||
return null;
|
||
}
|
||
|
||
let value = rawValue.trim();
|
||
if (!value) {
|
||
return null;
|
||
}
|
||
|
||
const trackingIndex = value.indexOf('__cft__');
|
||
if (trackingIndex !== -1) {
|
||
value = value.slice(0, trackingIndex);
|
||
}
|
||
|
||
value = value.replace(/[?&]$/, '');
|
||
|
||
let parsed;
|
||
try {
|
||
parsed = new URL(value);
|
||
} catch (error) {
|
||
try {
|
||
parsed = new URL(value, window.location.origin);
|
||
} catch (innerError) {
|
||
try {
|
||
parsed = new URL(value, 'https://www.facebook.com');
|
||
} catch (fallbackError) {
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!parsed.hostname.toLowerCase().endsWith('facebook.com')) {
|
||
return null;
|
||
}
|
||
|
||
const cleanedParams = new URLSearchParams();
|
||
parsed.searchParams.forEach((paramValue, paramKey) => {
|
||
const lowerKey = paramKey.toLowerCase();
|
||
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);
|
||
});
|
||
|
||
const search = cleanedParams.toString();
|
||
const formatted = `${parsed.origin}${parsed.pathname}${search ? `?${search}` : ''}`;
|
||
return formatted.replace(/[?&]$/, '');
|
||
}
|
||
|
||
function getDeadlinePartsFromValue(value) {
|
||
const localValue = toDateTimeLocalValue(value);
|
||
|
||
if (!localValue) {
|
||
return {
|
||
date: '',
|
||
time: ''
|
||
};
|
||
}
|
||
|
||
const [datePart, timePart = ''] = localValue.split('T');
|
||
return {
|
||
date: datePart,
|
||
time: timePart
|
||
};
|
||
}
|
||
|
||
function getDefaultDeadlineParts() {
|
||
const tomorrow = new Date();
|
||
tomorrow.setHours(0, 0, 0, 0);
|
||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||
|
||
const localIso = new Date(tomorrow.getTime() - tomorrow.getTimezoneOffset() * 60000)
|
||
.toISOString()
|
||
.slice(0, 16);
|
||
|
||
const [datePart, timePart = '00:00'] = localIso.split('T');
|
||
|
||
return {
|
||
date: datePart,
|
||
time: timePart || '00:00'
|
||
};
|
||
}
|
||
|
||
function getDefaultDeadlineInputValue() {
|
||
const { date, time } = getDefaultDeadlineParts();
|
||
if (!date) {
|
||
return '';
|
||
}
|
||
return `${date}T${time || '00:00'}`;
|
||
}
|
||
|
||
function positionDeadlinePicker(picker, triggerElement) {
|
||
if (!picker || !triggerElement) {
|
||
return;
|
||
}
|
||
|
||
const rect = triggerElement.getBoundingClientRect();
|
||
const pickerRect = picker.getBoundingClientRect();
|
||
|
||
const viewportWidth = window.innerWidth;
|
||
const viewportHeight = window.innerHeight;
|
||
const safeMargin = 16;
|
||
const offset = 8;
|
||
|
||
let left = rect.left;
|
||
let top = rect.bottom + offset;
|
||
|
||
if (left + pickerRect.width + safeMargin > viewportWidth) {
|
||
left = viewportWidth - pickerRect.width - safeMargin;
|
||
}
|
||
|
||
if (left < safeMargin) {
|
||
left = safeMargin;
|
||
}
|
||
|
||
if (top + pickerRect.height + safeMargin > viewportHeight) {
|
||
top = rect.top - pickerRect.height - offset;
|
||
if (top < safeMargin) {
|
||
top = viewportHeight - pickerRect.height - safeMargin;
|
||
}
|
||
}
|
||
|
||
if (top < safeMargin) {
|
||
top = safeMargin;
|
||
}
|
||
|
||
picker.style.left = `${Math.round(left)}px`;
|
||
picker.style.top = `${Math.round(top)}px`;
|
||
}
|
||
|
||
function closeActiveDeadlinePicker() {
|
||
if (!activeDeadlinePicker) {
|
||
return;
|
||
}
|
||
|
||
const current = activeDeadlinePicker;
|
||
activeDeadlinePicker = null;
|
||
|
||
try {
|
||
if (typeof current.destroy === 'function') {
|
||
current.destroy();
|
||
return;
|
||
}
|
||
|
||
const { input } = current;
|
||
if (input) {
|
||
input.removeEventListener('change', current.onChange);
|
||
input.removeEventListener('blur', current.onBlur);
|
||
input.remove();
|
||
}
|
||
} catch (error) {
|
||
console.warn('Konnte Deadline-Picker nicht schließen:', error);
|
||
}
|
||
}
|
||
|
||
function openNativeDeadlinePicker(post, triggerElement) {
|
||
if (!post || !triggerElement) {
|
||
return;
|
||
}
|
||
|
||
closeActiveDeadlinePicker();
|
||
|
||
const picker = document.createElement('div');
|
||
picker.className = 'deadline-picker';
|
||
picker.setAttribute('role', 'dialog');
|
||
picker.setAttribute('aria-modal', 'true');
|
||
picker.setAttribute('tabindex', '-1');
|
||
|
||
picker.innerHTML = `
|
||
<div class="deadline-picker__header">
|
||
<span class="deadline-picker__title">Deadline anpassen</span>
|
||
<button type="button" class="deadline-picker__close" aria-label="Schließen">×</button>
|
||
</div>
|
||
<form class="deadline-picker__form" novalidate>
|
||
<div class="deadline-picker__field">
|
||
<label>
|
||
<span>Datum</span>
|
||
<input type="date" class="deadline-picker__date" required>
|
||
</label>
|
||
</div>
|
||
<div class="deadline-picker__field">
|
||
<label>
|
||
<span>Uhrzeit</span>
|
||
<input type="time" class="deadline-picker__time" step="900" placeholder="hh:mm">
|
||
</label>
|
||
</div>
|
||
<div class="deadline-picker__hint">Uhrzeit optional – Standard ist 00:00 Uhr.</div>
|
||
<div class="deadline-picker__error" aria-live="polite"></div>
|
||
<div class="deadline-picker__actions">
|
||
<button type="submit" class="btn btn-primary deadline-picker__save">Speichern</button>
|
||
<button type="button" class="btn btn-secondary deadline-picker__cancel">Abbrechen</button>
|
||
</div>
|
||
</form>
|
||
`;
|
||
|
||
const form = picker.querySelector('.deadline-picker__form');
|
||
const dateInput = picker.querySelector('.deadline-picker__date');
|
||
const timeInput = picker.querySelector('.deadline-picker__time');
|
||
const errorEl = picker.querySelector('.deadline-picker__error');
|
||
const cancelButton = picker.querySelector('.deadline-picker__cancel');
|
||
const closeButton = picker.querySelector('.deadline-picker__close');
|
||
const saveButton = picker.querySelector('.deadline-picker__save');
|
||
|
||
const originalSaveLabel = saveButton ? saveButton.textContent : '';
|
||
const initialValues = { date: '', time: '' };
|
||
|
||
const applyInitialValues = () => {
|
||
const existing = getDeadlinePartsFromValue(post.deadline_at);
|
||
const defaults = existing.date ? existing : getDefaultDeadlineParts();
|
||
|
||
initialValues.date = defaults.date;
|
||
initialValues.time = defaults.time || '';
|
||
|
||
if (dateInput) {
|
||
dateInput.value = defaults.date;
|
||
}
|
||
if (timeInput) {
|
||
timeInput.value = defaults.time;
|
||
}
|
||
};
|
||
|
||
applyInitialValues();
|
||
|
||
document.body.appendChild(picker);
|
||
positionDeadlinePicker(picker, triggerElement);
|
||
|
||
const showError = (message = '') => {
|
||
if (!errorEl) {
|
||
return;
|
||
}
|
||
errorEl.textContent = message;
|
||
errorEl.classList.toggle('is-visible', Boolean(message));
|
||
};
|
||
|
||
let isSaving = false;
|
||
|
||
const normalizeParts = (datePart, timePart) => {
|
||
if (!datePart) {
|
||
return '';
|
||
}
|
||
return `${datePart}T${(timePart || '00:00')}`;
|
||
};
|
||
|
||
const attemptAutoSave = () => {
|
||
if (isSaving || !form || !dateInput) {
|
||
return false;
|
||
}
|
||
|
||
const currentDate = dateInput.value;
|
||
if (!currentDate) {
|
||
return false;
|
||
}
|
||
|
||
const currentTime = timeInput ? timeInput.value : '';
|
||
const currentNormalized = normalizeParts(currentDate, currentTime);
|
||
const initialNormalized = normalizeParts(initialValues.date, initialValues.time);
|
||
|
||
if (currentNormalized === initialNormalized) {
|
||
return false;
|
||
}
|
||
|
||
if (typeof form.requestSubmit === 'function') {
|
||
form.requestSubmit();
|
||
} else {
|
||
form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
const handleSubmit = async (event) => {
|
||
event.preventDefault();
|
||
if (isSaving || !dateInput || !saveButton) {
|
||
return;
|
||
}
|
||
|
||
showError('');
|
||
|
||
const dateValue = dateInput.value;
|
||
if (!dateValue) {
|
||
showError('Bitte ein Datum auswählen.');
|
||
dateInput.focus();
|
||
return;
|
||
}
|
||
|
||
const timeValue = (timeInput && timeInput.value) ? timeInput.value : '00:00';
|
||
const combined = `${dateValue}T${timeValue || '00:00'}`;
|
||
const normalized = normalizeDeadlineInput(combined);
|
||
|
||
if (!normalized) {
|
||
showError('Ungültige Eingabe.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
isSaving = true;
|
||
saveButton.disabled = true;
|
||
saveButton.textContent = 'Speichert…';
|
||
const success = await saveDeadline(post.id, normalized);
|
||
if (success) {
|
||
closeActiveDeadlinePicker();
|
||
return;
|
||
}
|
||
showError('Deadline wurde nicht gespeichert.');
|
||
} catch (error) {
|
||
console.warn('Deadline konnte nicht gespeichert werden:', error);
|
||
showError('Konnte Deadline nicht speichern.');
|
||
} finally {
|
||
isSaving = false;
|
||
saveButton.disabled = false;
|
||
saveButton.textContent = originalSaveLabel;
|
||
}
|
||
};
|
||
|
||
form?.addEventListener('submit', handleSubmit);
|
||
|
||
const cleanupListeners = [];
|
||
|
||
const pickerState = {
|
||
destroy: () => {
|
||
while (cleanupListeners.length) {
|
||
const clean = cleanupListeners.pop();
|
||
try {
|
||
clean();
|
||
} catch (error) {
|
||
console.warn('Konnte Listener nicht entfernen:', error);
|
||
}
|
||
}
|
||
picker.remove();
|
||
}
|
||
};
|
||
|
||
const registerListener = (target, type, listener, options) => {
|
||
if (!target) {
|
||
return;
|
||
}
|
||
target.addEventListener(type, listener, options);
|
||
cleanupListeners.push(() => target.removeEventListener(type, listener, options));
|
||
};
|
||
|
||
const closePicker = () => {
|
||
if (activeDeadlinePicker) {
|
||
closeActiveDeadlinePicker();
|
||
return;
|
||
}
|
||
|
||
pickerState.destroy();
|
||
};
|
||
|
||
registerListener(cancelButton, 'click', (event) => {
|
||
event.preventDefault();
|
||
closePicker();
|
||
});
|
||
|
||
registerListener(closeButton, 'click', (event) => {
|
||
event.preventDefault();
|
||
closePicker();
|
||
});
|
||
|
||
const handleOutsidePointer = (event) => {
|
||
const targetElement = event.target instanceof Element ? event.target : null;
|
||
const isClearButton = targetElement?.closest('.post-deadline__clear');
|
||
|
||
if (!picker.contains(event.target) && !triggerElement.contains(event.target)) {
|
||
if (!isClearButton && attemptAutoSave()) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
return;
|
||
}
|
||
|
||
closePicker();
|
||
}
|
||
};
|
||
|
||
const handleKeydown = (event) => {
|
||
if (event.key === 'Escape') {
|
||
event.preventDefault();
|
||
closePicker();
|
||
}
|
||
};
|
||
|
||
const reposition = () => {
|
||
positionDeadlinePicker(picker, triggerElement);
|
||
};
|
||
|
||
registerListener(document, 'pointerdown', handleOutsidePointer, true);
|
||
registerListener(document, 'keydown', handleKeydown);
|
||
registerListener(window, 'resize', reposition);
|
||
registerListener(window, 'scroll', reposition, true);
|
||
|
||
activeDeadlinePicker = pickerState;
|
||
|
||
requestAnimationFrame(() => {
|
||
positionDeadlinePicker(picker, triggerElement);
|
||
requestAnimationFrame(() => {
|
||
dateInput?.focus({ preventScroll: true });
|
||
});
|
||
});
|
||
}
|
||
|
||
async function clearDeadline(postId) {
|
||
closeActiveDeadlinePicker();
|
||
await saveDeadline(postId, null);
|
||
}
|
||
|
||
|
||
function normalizeChecks(checks) {
|
||
if (!Array.isArray(checks)) {
|
||
return [];
|
||
}
|
||
|
||
return checks
|
||
.map((check) => {
|
||
if (!check) {
|
||
return null;
|
||
}
|
||
|
||
const parsed = parseInt(check.profile_number, 10);
|
||
if (Number.isNaN(parsed)) {
|
||
return null;
|
||
}
|
||
|
||
const profileNumber = Math.min(MAX_PROFILES, Math.max(1, parsed));
|
||
|
||
return {
|
||
...check,
|
||
profile_number: profileNumber,
|
||
profile_name: check.profile_name || getProfileName(profileNumber),
|
||
checked_at: check.checked_at || null
|
||
};
|
||
})
|
||
.filter(Boolean)
|
||
.sort((a, b) => {
|
||
const aTime = a.checked_at ? new Date(a.checked_at).getTime() : 0;
|
||
const bTime = b.checked_at ? new Date(b.checked_at).getTime() : 0;
|
||
if (aTime === bTime) {
|
||
return a.profile_number - b.profile_number;
|
||
}
|
||
return aTime - bTime;
|
||
});
|
||
}
|
||
|
||
function calculateUrgencyScore(postItem) {
|
||
const post = postItem.post;
|
||
const status = postItem.status;
|
||
|
||
// Remaining participations needed
|
||
const remaining = status.targetCount - status.checkedCount;
|
||
if (remaining <= 0) return 999999; // Completed posts go to bottom
|
||
|
||
const now = Date.now();
|
||
const createdAt = post.created_at ? new Date(post.created_at).getTime() : now;
|
||
|
||
// Time until deadline (in hours)
|
||
let hoursUntilDeadline = Infinity;
|
||
let totalDuration = Infinity;
|
||
if (post.deadline_at) {
|
||
const deadline = new Date(post.deadline_at).getTime();
|
||
hoursUntilDeadline = Math.max(0, (deadline - now) / (1000 * 60 * 60));
|
||
totalDuration = Math.max(1, (deadline - createdAt) / (1000 * 60 * 60)); // Total hours from creation to deadline
|
||
}
|
||
|
||
// Time since last participation (in hours)
|
||
let hoursSinceLastCheck = Infinity;
|
||
if (status.lastCheckedAt) {
|
||
const lastCheck = new Date(status.lastCheckedAt).getTime();
|
||
hoursSinceLastCheck = (now - lastCheck) / (1000 * 60 * 60);
|
||
}
|
||
|
||
// Calculate ideal pace: how often should participations happen?
|
||
let idealIntervalHours = Infinity;
|
||
let behindSchedule = false;
|
||
|
||
if (totalDuration < Infinity && status.targetCount > 0) {
|
||
// Ideal interval between participations
|
||
idealIntervalHours = totalDuration / status.targetCount;
|
||
|
||
// Expected participations by now (based on time elapsed)
|
||
const hoursElapsed = (now - createdAt) / (1000 * 60 * 60);
|
||
const expectedChecks = Math.floor(hoursElapsed / idealIntervalHours);
|
||
|
||
// Are we behind schedule?
|
||
behindSchedule = status.checkedCount < expectedChecks;
|
||
}
|
||
|
||
// Calculate urgency score (lower = more urgent)
|
||
let score = 0;
|
||
|
||
// For posts with deadline: calculate when the next participation should ideally happen
|
||
if (hoursUntilDeadline < Infinity && remaining > 0) {
|
||
// How many hours do we have per remaining participation?
|
||
const hoursPerParticipation = hoursUntilDeadline / remaining;
|
||
|
||
// When should the next participation ideally happen?
|
||
let hoursUntilNextIdeal = hoursPerParticipation;
|
||
|
||
// If we have a last check, calculate from there
|
||
if (hoursSinceLastCheck < Infinity) {
|
||
hoursUntilNextIdeal = Math.max(0, hoursPerParticipation - hoursSinceLastCheck);
|
||
}
|
||
|
||
// Score based on how soon the next participation is due
|
||
// Posts that are overdue or due soon get higher priority
|
||
if (hoursUntilNextIdeal <= 0) {
|
||
// Overdue! High priority
|
||
score = Math.abs(hoursUntilNextIdeal) * -10; // Negative score = very high priority
|
||
} else if (hoursUntilNextIdeal <= 24) {
|
||
// Due within 24h
|
||
score = hoursUntilNextIdeal * 10; // 0-240
|
||
} else if (hoursUntilNextIdeal <= 72) {
|
||
// Due within 3 days
|
||
score = 240 + (hoursUntilNextIdeal - 24) * 20; // 240-1200
|
||
} else {
|
||
// Due later
|
||
score = 1200 + (hoursUntilNextIdeal - 72) * 50; // 1200+
|
||
}
|
||
|
||
// Emergency boost: if deadline is very close, override everything
|
||
if (hoursUntilDeadline <= 24 && remaining > 0) {
|
||
score = Math.min(score, hoursUntilDeadline * 5); // Max 120 points
|
||
} else if (hoursUntilDeadline <= 48 && remaining > 1) {
|
||
score = Math.min(score, 120 + (hoursUntilDeadline - 24) * 10);
|
||
}
|
||
} else if (hoursUntilDeadline < Infinity && remaining === 0) {
|
||
// Completed with deadline
|
||
score = 100000;
|
||
} else {
|
||
// No deadline - use simpler heuristic
|
||
score = 50000;
|
||
|
||
// Prioritize posts that haven't been checked in a while
|
||
if (hoursSinceLastCheck < Infinity) {
|
||
if (hoursSinceLastCheck < 24) {
|
||
score += 50; // Recently checked = lower priority
|
||
} else if (hoursSinceLastCheck > 72) {
|
||
score -= 100; // Long time since check = higher priority
|
||
}
|
||
}
|
||
}
|
||
|
||
// Recent participation (<24h) should lower urgency regardless of deadline pressure
|
||
if (hoursSinceLastCheck < 24) {
|
||
const freshnessRatio = (24 - hoursSinceLastCheck) / 24; // 0 (at 24h) to 1 (just now)
|
||
const recencyPenalty = 60 + Math.round(freshnessRatio * 120); // 60-180 point penalty
|
||
score += recencyPenalty;
|
||
}
|
||
|
||
// Give extra priority to posts with deadlines approaching soon
|
||
if (hoursUntilDeadline < Infinity && remaining > 0) {
|
||
const urgencyWindowHours = 72;
|
||
const cappedHours = Math.min(hoursUntilDeadline, urgencyWindowHours);
|
||
const closenessRatio = 1 - (cappedHours / urgencyWindowHours); // 0-1
|
||
|
||
if (closenessRatio > 0) {
|
||
const baseBoost = Math.round(closenessRatio * 120); // Up to 120 points
|
||
const remainingFactor = Math.min(1.6, 0.7 + remaining * 0.3); // 1.0 (1 remaining) .. 1.6 (>=3 remaining)
|
||
const deadlineBoost = Math.round(baseBoost * remainingFactor);
|
||
score -= deadlineBoost;
|
||
}
|
||
}
|
||
|
||
return score;
|
||
}
|
||
|
||
function comparePostItems(a, b) {
|
||
const postA = a.post;
|
||
const postB = b.post;
|
||
const createdA = toTimestamp(postA.created_at, 0);
|
||
const createdB = toTimestamp(postB.created_at, 0);
|
||
|
||
let comparison = 0;
|
||
|
||
if (sortMode === 'deadline') {
|
||
const deadlineA = toTimestamp(postA.deadline_at, Infinity);
|
||
const deadlineB = toTimestamp(postB.deadline_at, Infinity);
|
||
|
||
if (deadlineA !== deadlineB) {
|
||
comparison = deadlineA - deadlineB;
|
||
}
|
||
} else if (sortMode === 'smart') {
|
||
// Smart sorting based on urgency
|
||
const scoreA = calculateUrgencyScore(a);
|
||
const scoreB = calculateUrgencyScore(b);
|
||
|
||
if (scoreA !== scoreB) {
|
||
comparison = scoreB - scoreA; // Higher score = lower priority (inverted for intuitive direction)
|
||
}
|
||
} else if (sortMode === 'lastChange') {
|
||
const changeA = Number.isFinite(a.status.lastChangeTimestamp)
|
||
? a.status.lastChangeTimestamp
|
||
: toTimestamp(postA.created_at, 0);
|
||
const changeB = Number.isFinite(b.status.lastChangeTimestamp)
|
||
? b.status.lastChangeTimestamp
|
||
: toTimestamp(postB.created_at, 0);
|
||
|
||
if (changeA !== changeB) {
|
||
comparison = changeA - changeB;
|
||
}
|
||
} else if (sortMode === 'lastCheck') {
|
||
const lastA = Number.isFinite(a.status.lastCheckedTimestamp)
|
||
? a.status.lastCheckedTimestamp
|
||
: Number.NEGATIVE_INFINITY;
|
||
const lastB = Number.isFinite(b.status.lastCheckedTimestamp)
|
||
? b.status.lastCheckedTimestamp
|
||
: Number.NEGATIVE_INFINITY;
|
||
|
||
if (lastA !== lastB) {
|
||
comparison = lastA - lastB;
|
||
}
|
||
} else if (createdA !== createdB) {
|
||
comparison = createdA - createdB;
|
||
}
|
||
|
||
if (comparison === 0 && createdA !== createdB) {
|
||
comparison = createdA - createdB;
|
||
}
|
||
|
||
if (comparison === 0) {
|
||
const idA = String(postA.id || '');
|
||
const idB = String(postB.id || '');
|
||
if (idA !== idB) {
|
||
comparison = idA < idB ? -1 : 1;
|
||
}
|
||
}
|
||
|
||
if (comparison === 0) {
|
||
return 0;
|
||
}
|
||
|
||
const multiplier = sortDirection === 'asc' ? 1 : -1;
|
||
return comparison * multiplier;
|
||
}
|
||
|
||
function displayManualPostMessage(message, type = 'success') {
|
||
if (!manualPostMessage) {
|
||
return;
|
||
}
|
||
|
||
manualPostMessage.textContent = message;
|
||
manualPostMessage.classList.remove('error', 'success');
|
||
manualPostMessage.classList.add(type);
|
||
}
|
||
|
||
function clearManualPostMessage() {
|
||
if (!manualPostMessage) {
|
||
return;
|
||
}
|
||
manualPostMessage.textContent = '';
|
||
manualPostMessage.classList.remove('error', 'success');
|
||
}
|
||
|
||
function openManualPostModal({ mode = 'create', post = null, focus = null } = {}) {
|
||
if (!manualPostModal || !manualPostForm) {
|
||
return;
|
||
}
|
||
|
||
manualPostModalLastFocus = document.activeElement && typeof document.activeElement.blur === 'function'
|
||
? document.activeElement
|
||
: null;
|
||
|
||
resetManualPostForm({ keepMessages: false });
|
||
|
||
manualPostMode = mode === 'edit' ? 'edit' : 'create';
|
||
manualPostEditingId = manualPostMode === 'edit' && post ? post.id : null;
|
||
|
||
const desiredFocus = typeof focus === 'string' ? focus : (manualPostMode === 'edit' ? 'title' : 'url');
|
||
|
||
if (manualPostModalTitle) {
|
||
manualPostModalTitle.textContent = manualPostMode === 'edit'
|
||
? 'Beitrag bearbeiten'
|
||
: 'Beitrag hinzufügen';
|
||
}
|
||
|
||
if (manualPostSubmitButton) {
|
||
manualPostSubmitButton.textContent = manualPostMode === 'edit'
|
||
? 'Aktualisieren'
|
||
: 'Speichern';
|
||
}
|
||
|
||
if (manualPostMode === 'edit' && post) {
|
||
populateManualPostForm(post);
|
||
}
|
||
|
||
manualPostModal.removeAttribute('hidden');
|
||
manualPostModal.classList.add('open');
|
||
|
||
manualPostModalPreviousOverflow = document.body.style.overflow;
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
requestAnimationFrame(() => {
|
||
if (manualPostModalContent) {
|
||
manualPostModalContent.focus();
|
||
}
|
||
|
||
if (desiredFocus === 'deadline' && manualPostDeadlineInput) {
|
||
manualPostDeadlineInput.focus();
|
||
manualPostDeadlineInput.select?.();
|
||
return;
|
||
}
|
||
|
||
if (desiredFocus === 'title' && manualPostTitleInput) {
|
||
manualPostTitleInput.focus();
|
||
manualPostTitleInput.select();
|
||
return;
|
||
}
|
||
|
||
if (desiredFocus === 'url' && manualPostUrlInput) {
|
||
manualPostUrlInput.focus();
|
||
manualPostUrlInput.select?.();
|
||
return;
|
||
}
|
||
|
||
if (manualPostMode === 'edit' && manualPostTitleInput) {
|
||
manualPostTitleInput.focus();
|
||
manualPostTitleInput.select();
|
||
} else if (manualPostUrlInput) {
|
||
manualPostUrlInput.focus();
|
||
}
|
||
});
|
||
}
|
||
|
||
function closeManualPostModal() {
|
||
if (!manualPostModal) {
|
||
return;
|
||
}
|
||
|
||
manualPostModal.classList.remove('open');
|
||
manualPostModal.setAttribute('hidden', '');
|
||
resetManualPostForm();
|
||
|
||
document.body.style.overflow = manualPostModalPreviousOverflow;
|
||
|
||
if (manualPostModalLastFocus && typeof manualPostModalLastFocus.focus === 'function') {
|
||
manualPostModalLastFocus.focus();
|
||
}
|
||
}
|
||
|
||
function loadAutoRefreshSettings() {
|
||
try {
|
||
const stored = localStorage.getItem(REFRESH_SETTINGS_KEY);
|
||
if (stored) {
|
||
const parsed = JSON.parse(stored);
|
||
if (typeof parsed === 'object' && parsed) {
|
||
if (typeof parsed.enabled === 'boolean') {
|
||
autoRefreshSettings.enabled = parsed.enabled;
|
||
}
|
||
if (typeof parsed.interval === 'number' && parsed.interval >= 5000) {
|
||
autoRefreshSettings.interval = parsed.interval;
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn('Konnte Refresh-Einstellungen nicht laden:', error);
|
||
}
|
||
|
||
if (autoRefreshToggle) {
|
||
autoRefreshToggle.checked = autoRefreshSettings.enabled;
|
||
}
|
||
if (autoRefreshIntervalSelect) {
|
||
autoRefreshIntervalSelect.value = String(autoRefreshSettings.interval);
|
||
autoRefreshIntervalSelect.disabled = !autoRefreshSettings.enabled;
|
||
}
|
||
}
|
||
|
||
function saveAutoRefreshSettings() {
|
||
try {
|
||
localStorage.setItem(REFRESH_SETTINGS_KEY, JSON.stringify(autoRefreshSettings));
|
||
} catch (error) {
|
||
console.warn('Konnte Refresh-Einstellungen nicht speichern:', error);
|
||
}
|
||
}
|
||
|
||
function applyAutoRefreshSettings() {
|
||
if (autoRefreshTimer) {
|
||
clearInterval(autoRefreshTimer);
|
||
autoRefreshTimer = null;
|
||
}
|
||
|
||
if (!autoRefreshSettings.enabled) {
|
||
return;
|
||
}
|
||
|
||
autoRefreshTimer = setInterval(() => {
|
||
if (document.hidden) {
|
||
return;
|
||
}
|
||
fetchPosts({ showLoader: false });
|
||
}, autoRefreshSettings.interval);
|
||
}
|
||
|
||
function loadSortMode({ fromTabChange = false } = {}) {
|
||
const pageKey = getSortSettingsPageKey();
|
||
const tabKey = getSortTabKey();
|
||
const storage = getSortStorage();
|
||
const pageSettings = storage[pageKey] && typeof storage[pageKey] === 'object' ? storage[pageKey] : {};
|
||
|
||
const legacyCandidate = (pageSettings.mode || pageSettings.direction)
|
||
? { mode: pageSettings.mode, direction: pageSettings.direction }
|
||
: null;
|
||
|
||
const candidates = [pageSettings[tabKey], legacyCandidate, pageSettings.default, storage.default];
|
||
let applied = null;
|
||
|
||
for (const candidate of candidates) {
|
||
if (candidate && typeof candidate === 'object') {
|
||
applied = candidate;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (applied) {
|
||
sortMode = normalizeSortMode(applied.mode);
|
||
sortDirection = normalizeSortDirection(applied.direction);
|
||
} else {
|
||
sortMode = normalizeSortMode(sortMode);
|
||
sortDirection = normalizeSortDirection(sortDirection);
|
||
}
|
||
|
||
if (sortModeSelect) {
|
||
sortModeSelect.value = sortMode;
|
||
}
|
||
updateSortDirectionToggleUI();
|
||
|
||
if (!fromTabChange) {
|
||
// Ensure structure exists for initial load
|
||
saveSortMode();
|
||
}
|
||
}
|
||
|
||
function saveSortMode() {
|
||
const pageKey = getSortSettingsPageKey();
|
||
const tabKey = getSortTabKey();
|
||
const storage = getSortStorage();
|
||
|
||
if (!storage[pageKey] || typeof storage[pageKey] !== 'object') {
|
||
storage[pageKey] = {};
|
||
}
|
||
|
||
delete storage[pageKey].mode;
|
||
delete storage[pageKey].direction;
|
||
|
||
storage[pageKey][tabKey] = {
|
||
mode: normalizeSortMode(sortMode),
|
||
direction: normalizeSortDirection(sortDirection)
|
||
};
|
||
|
||
if (!storage[pageKey].default) {
|
||
storage[pageKey].default = { ...DEFAULT_SORT_SETTINGS };
|
||
}
|
||
|
||
if (!storage.default) {
|
||
storage.default = { ...DEFAULT_SORT_SETTINGS };
|
||
}
|
||
|
||
persistSortStorage(storage);
|
||
}
|
||
|
||
function computePostStatus(post, profileNumber = currentProfile) {
|
||
const requiredProfiles = normalizeRequiredProfiles(post);
|
||
const checks = normalizeChecks(post.checks);
|
||
|
||
let lastCheckedAt = null;
|
||
let lastCheckedTimestamp = null;
|
||
|
||
for (const check of checks) {
|
||
if (!check || !check.checked_at) {
|
||
continue;
|
||
}
|
||
const timestamp = new Date(check.checked_at).getTime();
|
||
if (Number.isNaN(timestamp)) {
|
||
continue;
|
||
}
|
||
if (lastCheckedTimestamp === null || timestamp > lastCheckedTimestamp) {
|
||
lastCheckedTimestamp = timestamp;
|
||
lastCheckedAt = check.checked_at;
|
||
}
|
||
}
|
||
|
||
const lastChangeTimestamp = toTimestamp(post.last_change, toTimestamp(post.created_at, 0));
|
||
const lastChangeAt = post.last_change || post.created_at || null;
|
||
|
||
// Check if post is expired (deadline passed)
|
||
const isExpired = post.deadline_at ? new Date(post.deadline_at) < new Date() : false;
|
||
|
||
const backendStatuses = Array.isArray(post.profile_statuses) ? post.profile_statuses : [];
|
||
let profileStatuses = backendStatuses
|
||
.map((status) => {
|
||
if (!status) {
|
||
return null;
|
||
}
|
||
const parsed = parseInt(status.profile_number, 10);
|
||
if (Number.isNaN(parsed)) {
|
||
return null;
|
||
}
|
||
const profileNumberValue = Math.min(MAX_PROFILES, Math.max(1, parsed));
|
||
let normalizedStatus = status.status;
|
||
if (normalizedStatus !== 'done' && normalizedStatus !== 'available') {
|
||
normalizedStatus = 'locked';
|
||
}
|
||
const check = checks.find((item) => item.profile_number === profileNumberValue) || null;
|
||
return {
|
||
profile_number: profileNumberValue,
|
||
profile_name: status.profile_name || getProfileName(profileNumberValue),
|
||
status: normalizedStatus,
|
||
checked_at: status.checked_at || (check ? check.checked_at : null) || null
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
|
||
if (profileStatuses.length !== requiredProfiles.length) {
|
||
const checksByProfile = new Map(checks.map((check) => [check.profile_number, check]));
|
||
const completedSet = new Set(checks.map((check) => check.profile_number));
|
||
|
||
profileStatuses = requiredProfiles.map((value, index) => {
|
||
const prerequisites = requiredProfiles.slice(0, index);
|
||
const prerequisitesMet = prerequisites.every((profile) => completedSet.has(profile));
|
||
const check = checksByProfile.get(value) || null;
|
||
|
||
return {
|
||
profile_number: value,
|
||
profile_name: getProfileName(value),
|
||
status: check ? 'done' : (prerequisitesMet ? 'available' : 'locked'),
|
||
checked_at: check ? check.checked_at : null
|
||
};
|
||
});
|
||
} else {
|
||
const checksByProfile = new Map(checks.map((check) => [check.profile_number, check]));
|
||
profileStatuses = requiredProfiles.map((value) => {
|
||
const status = profileStatuses.find((item) => item.profile_number === value);
|
||
if (!status) {
|
||
const check = checksByProfile.get(value) || null;
|
||
return {
|
||
profile_number: value,
|
||
profile_name: getProfileName(value),
|
||
status: check ? 'done' : 'locked',
|
||
checked_at: check ? check.checked_at : null
|
||
};
|
||
}
|
||
|
||
if (status.status === 'done') {
|
||
const check = checksByProfile.get(value) || null;
|
||
return {
|
||
...status,
|
||
checked_at: status.checked_at || (check ? check.checked_at : null) || null
|
||
};
|
||
}
|
||
|
||
if (status.status === 'available') {
|
||
return {
|
||
...status,
|
||
checked_at: status.checked_at || null
|
||
};
|
||
}
|
||
|
||
return {
|
||
...status,
|
||
status: 'locked',
|
||
checked_at: status.checked_at || null
|
||
};
|
||
});
|
||
}
|
||
|
||
const completedProfilesSet = new Set(
|
||
profileStatuses
|
||
.filter((status) => status.status === 'done')
|
||
.map((status) => status.profile_number)
|
||
);
|
||
|
||
const checkedCount = profileStatuses.filter((status) => status.status === 'done').length;
|
||
const targetCount = profileStatuses.length;
|
||
const isComplete = profileStatuses.every((status) => status.status === 'done');
|
||
const nextRequiredProfile = profileStatuses.find((status) => status.status === 'available') || null;
|
||
const isCurrentProfileRequired = requiredProfiles.includes(profileNumber);
|
||
const isCurrentProfileDone = profileStatuses.some((status) => status.profile_number === profileNumber && status.status === 'done');
|
||
const canCurrentProfileCheck = profileStatuses.some((status) => status.profile_number === profileNumber && status.status === 'available');
|
||
|
||
const waitingForStatuses = profileStatuses.filter((status) => status.profile_number < profileNumber && status.status !== 'done');
|
||
const waitingForProfiles = waitingForStatuses.map((status) => status.profile_number);
|
||
const waitingForNames = waitingForStatuses.map((status) => status.profile_name);
|
||
|
||
return {
|
||
requiredProfiles,
|
||
profileStatuses,
|
||
checks,
|
||
lastCheckedAt,
|
||
lastCheckedTimestamp,
|
||
lastChangeAt,
|
||
lastChangeTimestamp,
|
||
completedProfilesSet,
|
||
checkedCount,
|
||
targetCount,
|
||
isComplete,
|
||
isCurrentProfileRequired,
|
||
isCurrentProfileDone,
|
||
canCurrentProfileCheck,
|
||
waitingForProfiles,
|
||
waitingForNames,
|
||
nextRequiredProfile,
|
||
profileNumber,
|
||
isExpired
|
||
};
|
||
}
|
||
|
||
function applyScreenshotModalSize() {
|
||
if (!screenshotModalContent || !screenshotModalImage) {
|
||
return;
|
||
}
|
||
|
||
if (screenshotModalZoomed) {
|
||
return;
|
||
}
|
||
|
||
if (!screenshotModalImage.src) {
|
||
return;
|
||
}
|
||
|
||
requestAnimationFrame(() => {
|
||
const padding = 48;
|
||
const viewportWidth = Math.max(320, window.innerWidth * 0.95);
|
||
const viewportHeight = Math.max(280, window.innerHeight * 0.92);
|
||
const naturalWidth = screenshotModalImage.naturalWidth || screenshotModalImage.width || 0;
|
||
const naturalHeight = screenshotModalImage.naturalHeight || screenshotModalImage.height || 0;
|
||
|
||
const targetWidth = Math.min(Math.max(320, naturalWidth + padding), viewportWidth);
|
||
const targetHeight = Math.min(Math.max(260, naturalHeight + padding), viewportHeight);
|
||
|
||
screenshotModalContent.style.width = `${targetWidth}px`;
|
||
screenshotModalContent.style.height = `${targetHeight}px`;
|
||
});
|
||
}
|
||
|
||
async function fetchProfileState() {
|
||
try {
|
||
const response = await apiFetch(`${API_URL}/profile-state`);
|
||
if (!response.ok) {
|
||
return null;
|
||
}
|
||
const data = await response.json();
|
||
if (data && typeof data.profile_number !== 'undefined') {
|
||
const parsed = parseInt(data.profile_number, 10);
|
||
if (!Number.isNaN(parsed)) {
|
||
return parsed;
|
||
}
|
||
}
|
||
return null;
|
||
} catch (error) {
|
||
console.warn('Profilstatus konnte nicht geladen werden:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function pushProfileState(profileNumber) {
|
||
try {
|
||
await apiFetch(`${API_URL}/profile-state`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ profile_number: profileNumber })
|
||
});
|
||
} catch (error) {
|
||
console.error('Profilstatus konnte nicht gespeichert werden:', error);
|
||
}
|
||
}
|
||
|
||
function applyProfileNumber(profileNumber, { fromBackend = false } = {}) {
|
||
if (!profileNumber) {
|
||
return;
|
||
}
|
||
|
||
document.getElementById('profileSelect').value = String(profileNumber);
|
||
|
||
if (currentProfile === profileNumber) {
|
||
if (!fromBackend) {
|
||
pushProfileState(profileNumber);
|
||
}
|
||
return;
|
||
}
|
||
|
||
currentProfile = profileNumber;
|
||
localStorage.setItem('profileNumber', currentProfile);
|
||
|
||
if (!fromBackend) {
|
||
pushProfileState(currentProfile);
|
||
}
|
||
|
||
resetVisibleCount();
|
||
renderPosts();
|
||
}
|
||
|
||
// Load profile from localStorage
|
||
function loadProfile() {
|
||
fetchProfileState().then((backendProfile) => {
|
||
if (backendProfile) {
|
||
applyProfileNumber(backendProfile, { fromBackend: true });
|
||
} else {
|
||
const saved = localStorage.getItem('profileNumber');
|
||
if (saved) {
|
||
applyProfileNumber(parseInt(saved, 10) || 1, { fromBackend: true });
|
||
} else {
|
||
applyProfileNumber(1, { fromBackend: true });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Save profile to localStorage
|
||
function saveProfile(profileNumber) {
|
||
applyProfileNumber(profileNumber);
|
||
}
|
||
|
||
function startProfilePolling() {
|
||
if (profilePollTimer) {
|
||
clearInterval(profilePollTimer);
|
||
}
|
||
|
||
profilePollTimer = setInterval(async () => {
|
||
const backendProfile = await fetchProfileState();
|
||
if (backendProfile && backendProfile !== currentProfile) {
|
||
applyProfileNumber(backendProfile, { fromBackend: true });
|
||
}
|
||
}, 5000);
|
||
}
|
||
|
||
// Profile selector change handler
|
||
document.getElementById('profileSelect').addEventListener('change', (e) => {
|
||
saveProfile(parseInt(e.target.value, 10));
|
||
});
|
||
|
||
// Tab switching
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
setTab(btn.dataset.tab, { updateUrl: true });
|
||
});
|
||
});
|
||
|
||
if (manualPostForm) {
|
||
manualPostForm.addEventListener('submit', handleManualPostSubmit);
|
||
}
|
||
|
||
if (manualPostResetButton) {
|
||
manualPostResetButton.addEventListener('click', () => {
|
||
if (manualPostMode === 'edit' && manualPostEditingId) {
|
||
const post = posts.find((item) => item.id === manualPostEditingId);
|
||
if (post) {
|
||
populateManualPostForm(post);
|
||
}
|
||
} else {
|
||
resetManualPostForm();
|
||
if (manualPostUrlInput) {
|
||
manualPostUrlInput.focus();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
if (autoRefreshToggle) {
|
||
autoRefreshToggle.addEventListener('change', () => {
|
||
autoRefreshSettings.enabled = !!autoRefreshToggle.checked;
|
||
if (autoRefreshIntervalSelect) {
|
||
autoRefreshIntervalSelect.disabled = !autoRefreshSettings.enabled;
|
||
}
|
||
saveAutoRefreshSettings();
|
||
applyAutoRefreshSettings();
|
||
if (autoRefreshSettings.enabled) {
|
||
fetchPosts({ showLoader: false });
|
||
}
|
||
});
|
||
}
|
||
|
||
if (autoRefreshIntervalSelect) {
|
||
autoRefreshIntervalSelect.addEventListener('change', () => {
|
||
const value = parseInt(autoRefreshIntervalSelect.value, 10);
|
||
if (!Number.isNaN(value) && value >= 5000) {
|
||
autoRefreshSettings.interval = value;
|
||
saveAutoRefreshSettings();
|
||
applyAutoRefreshSettings();
|
||
}
|
||
});
|
||
}
|
||
|
||
const manualRefreshBtn = document.getElementById('manualRefreshBtn');
|
||
if (manualRefreshBtn) {
|
||
manualRefreshBtn.addEventListener('click', () => {
|
||
fetchPosts();
|
||
});
|
||
}
|
||
|
||
const searchInput = document.getElementById('searchInput');
|
||
if (searchInput) {
|
||
searchInput.addEventListener('input', () => {
|
||
resetVisibleCount();
|
||
renderPosts();
|
||
});
|
||
}
|
||
|
||
if (sortModeSelect) {
|
||
sortModeSelect.addEventListener('change', () => {
|
||
const value = sortModeSelect.value;
|
||
sortMode = VALID_SORT_MODES.has(value) ? value : DEFAULT_SORT_SETTINGS.mode;
|
||
saveSortMode();
|
||
resetVisibleCount();
|
||
renderPosts();
|
||
});
|
||
}
|
||
|
||
if (sortDirectionToggle) {
|
||
sortDirectionToggle.addEventListener('click', () => {
|
||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||
updateSortDirectionToggleUI();
|
||
saveSortMode();
|
||
resetVisibleCount();
|
||
renderPosts();
|
||
});
|
||
}
|
||
|
||
if (openManualPostModalBtn) {
|
||
openManualPostModalBtn.addEventListener('click', () => {
|
||
openManualPostModal({ mode: 'create' });
|
||
});
|
||
}
|
||
|
||
if (manualPostModalClose) {
|
||
manualPostModalClose.addEventListener('click', closeManualPostModal);
|
||
}
|
||
|
||
if (manualPostModalBackdrop) {
|
||
manualPostModalBackdrop.addEventListener('click', closeManualPostModal);
|
||
}
|
||
|
||
// Fetch all posts
|
||
async function fetchPosts({ showLoader = true } = {}) {
|
||
if (isFetchingPosts) {
|
||
return;
|
||
}
|
||
|
||
isFetchingPosts = true;
|
||
|
||
try {
|
||
if (showLoader) {
|
||
showLoading();
|
||
}
|
||
|
||
const response = await apiFetch(`${API_URL}/posts`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch posts');
|
||
}
|
||
|
||
const data = await response.json();
|
||
posts = Array.isArray(data) ? data : [];
|
||
await normalizeLoadedPostUrls();
|
||
renderPosts();
|
||
} catch (error) {
|
||
if (showLoader) {
|
||
showError('Fehler beim Laden der Beiträge. Stelle sicher, dass das Backend läuft.');
|
||
}
|
||
console.error('Error fetching posts:', error);
|
||
} finally {
|
||
if (showLoader) {
|
||
hideLoading();
|
||
}
|
||
isFetchingPosts = false;
|
||
}
|
||
}
|
||
|
||
async function normalizeLoadedPostUrls() {
|
||
if (!Array.isArray(posts) || !posts.length) {
|
||
return false;
|
||
}
|
||
|
||
const candidates = posts
|
||
.map((post) => {
|
||
if (!post || !post.id || !post.url) {
|
||
return null;
|
||
}
|
||
const cleaned = normalizeFacebookPostUrl(post.url);
|
||
if (!cleaned || cleaned === post.url) {
|
||
return null;
|
||
}
|
||
return { id: post.id, cleaned };
|
||
})
|
||
.filter(Boolean);
|
||
|
||
if (!candidates.length) {
|
||
return false;
|
||
}
|
||
|
||
let changed = false;
|
||
|
||
for (const candidate of candidates) {
|
||
try {
|
||
const response = await apiFetch(`${API_URL}/posts/${candidate.id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ url: candidate.cleaned })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.warn(`Konnte URL für Beitrag ${candidate.id} nicht normalisieren.`);
|
||
continue;
|
||
}
|
||
|
||
const updatedPost = await response.json();
|
||
posts = posts.map((item) => (item.id === candidate.id ? updatedPost : item));
|
||
changed = true;
|
||
} catch (error) {
|
||
console.warn(`Fehler beim Normalisieren der URL für Beitrag ${candidate.id}:`, error);
|
||
}
|
||
}
|
||
|
||
return changed;
|
||
}
|
||
|
||
function doesPostMatchFocus(post) {
|
||
if (!post) {
|
||
return false;
|
||
}
|
||
if (focusPostIdParam && String(post.id) === focusPostIdParam) {
|
||
return true;
|
||
}
|
||
if (focusNormalizedUrl && post.url) {
|
||
const candidateNormalized = normalizeFacebookPostUrl(post.url) || post.url;
|
||
return candidateNormalized === focusNormalizedUrl;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function resolveFocusTargetInfo(items) {
|
||
if (!Array.isArray(items) || (!focusPostIdParam && !focusNormalizedUrl)) {
|
||
return { index: -1, post: null };
|
||
}
|
||
const index = items.findIndex(({ post }) => doesPostMatchFocus(post));
|
||
return {
|
||
index,
|
||
post: index !== -1 && items[index] ? items[index].post : null
|
||
};
|
||
}
|
||
|
||
function clearFocusParamsFromUrl() {
|
||
try {
|
||
const url = new URL(window.location.href);
|
||
let changed = false;
|
||
if (url.searchParams.has('postId')) {
|
||
url.searchParams.delete('postId');
|
||
changed = true;
|
||
}
|
||
if (url.searchParams.has('postUrl')) {
|
||
url.searchParams.delete('postUrl');
|
||
changed = true;
|
||
}
|
||
if (changed) {
|
||
const newQuery = url.searchParams.toString();
|
||
const newUrl = `${url.pathname}${newQuery ? `?${newQuery}` : ''}${url.hash}`;
|
||
window.history.replaceState({}, document.title, newUrl);
|
||
}
|
||
} catch (error) {
|
||
console.warn('Konnte Fokus-Parameter nicht aus URL entfernen:', error);
|
||
}
|
||
}
|
||
|
||
function highlightPostCard(post) {
|
||
if (!post || focusHandled) {
|
||
return;
|
||
}
|
||
|
||
const card = document.getElementById(`post-${post.id}`);
|
||
if (!card) {
|
||
return;
|
||
}
|
||
|
||
card.classList.add('post-card--highlight');
|
||
|
||
const hadTabIndex = card.hasAttribute('tabindex');
|
||
if (!hadTabIndex) {
|
||
card.setAttribute('tabindex', '-1');
|
||
card.dataset.fbTrackerTempTabindex = '1';
|
||
}
|
||
|
||
requestAnimationFrame(() => {
|
||
try {
|
||
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
} catch (error) {
|
||
console.warn('Konnte Karte nicht scrollen:', error);
|
||
}
|
||
try {
|
||
card.focus({ preventScroll: true });
|
||
} catch (error) {
|
||
// ignore focus errors
|
||
}
|
||
});
|
||
|
||
window.setTimeout(() => {
|
||
card.classList.remove('post-card--highlight');
|
||
if (card.dataset.fbTrackerTempTabindex === '1') {
|
||
card.removeAttribute('tabindex');
|
||
delete card.dataset.fbTrackerTempTabindex;
|
||
}
|
||
}, 4000);
|
||
|
||
focusPostIdParam = null;
|
||
focusPostUrlParam = null;
|
||
focusNormalizedUrl = '';
|
||
focusHandled = true;
|
||
focusTabAdjusted = null;
|
||
clearFocusParamsFromUrl();
|
||
}
|
||
|
||
// Render posts
|
||
function renderPosts() {
|
||
hideLoading();
|
||
hideError();
|
||
|
||
const container = document.getElementById('postsContainer');
|
||
if (!container) {
|
||
return;
|
||
}
|
||
|
||
closeActiveDeadlinePicker();
|
||
updateTabButtons();
|
||
cleanupLoadMoreObserver();
|
||
|
||
const postItems = posts.map((post) => ({
|
||
post,
|
||
status: computePostStatus(post)
|
||
}));
|
||
|
||
const sortedItems = [...postItems].sort(comparePostItems);
|
||
const focusCandidateEntry = (!focusHandled && (focusPostIdParam || focusNormalizedUrl))
|
||
? sortedItems.find((item) => doesPostMatchFocus(item.post))
|
||
: null;
|
||
|
||
let filteredItems = sortedItems;
|
||
|
||
if (currentTab === 'pending') {
|
||
filteredItems = sortedItems.filter((item) => !item.status.isExpired && item.status.canCurrentProfileCheck && !item.status.isComplete);
|
||
} else if (currentTab === 'expired') {
|
||
filteredItems = sortedItems.filter((item) => item.status.isExpired || item.status.isComplete);
|
||
} else if (currentTab === 'all') {
|
||
filteredItems = sortedItems.filter((item) => !item.status.isExpired && !item.status.isComplete);
|
||
}
|
||
|
||
const tabTotalCount = filteredItems.length;
|
||
|
||
const searchInput = document.getElementById('searchInput');
|
||
const searchValue = searchInput && typeof searchInput.value === 'string' ? searchInput.value.trim() : '';
|
||
const searchActive = Boolean(searchValue);
|
||
|
||
if (searchActive) {
|
||
const searchTerm = searchValue.toLowerCase();
|
||
filteredItems = filteredItems.filter((item) => {
|
||
const post = item.post;
|
||
return (
|
||
(post.title && post.title.toLowerCase().includes(searchTerm)) ||
|
||
(post.url && post.url.toLowerCase().includes(searchTerm)) ||
|
||
(post.created_by_name && post.created_by_name.toLowerCase().includes(searchTerm)) ||
|
||
(post.id && post.id.toLowerCase().includes(searchTerm))
|
||
);
|
||
});
|
||
}
|
||
|
||
if (!focusHandled && focusCandidateEntry && !searchActive) {
|
||
const candidateVisibleInCurrentTab = filteredItems.some(({ post }) => doesPostMatchFocus(post));
|
||
if (!candidateVisibleInCurrentTab) {
|
||
let desiredTab = 'all';
|
||
if (focusCandidateEntry.status.isExpired || focusCandidateEntry.status.isComplete) {
|
||
desiredTab = 'expired';
|
||
} else if (focusCandidateEntry.status.canCurrentProfileCheck && !focusCandidateEntry.status.isExpired && !focusCandidateEntry.status.isComplete) {
|
||
desiredTab = 'pending';
|
||
}
|
||
|
||
if (currentTab !== desiredTab && focusTabAdjusted !== desiredTab) {
|
||
focusTabAdjusted = desiredTab;
|
||
setTab(desiredTab);
|
||
return;
|
||
}
|
||
|
||
if (desiredTab !== 'all' && currentTab !== 'all' && focusTabAdjusted !== 'all') {
|
||
focusTabAdjusted = 'all';
|
||
setTab('all');
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
const focusTargetInfo = resolveFocusTargetInfo(filteredItems);
|
||
if (focusTargetInfo.index !== -1) {
|
||
const requiredVisible = focusTargetInfo.index + 1;
|
||
if (requiredVisible > getVisibleCount(currentTab)) {
|
||
setVisibleCount(currentTab, requiredVisible);
|
||
}
|
||
}
|
||
|
||
updateFilteredCount(currentTab, filteredItems.length);
|
||
|
||
const visibleCount = Math.min(filteredItems.length, getVisibleCount(currentTab));
|
||
const visibleItems = filteredItems.slice(0, visibleCount);
|
||
|
||
const summaryHtml = buildPostsSummary({
|
||
tab: currentTab,
|
||
visibleCount,
|
||
filteredCount: filteredItems.length,
|
||
tabTotalCount,
|
||
totalCountAll: posts.length,
|
||
searchActive
|
||
});
|
||
|
||
if (filteredItems.length === 0) {
|
||
let emptyMessage = 'Noch keine Beiträge erfasst.';
|
||
let emptyIcon = '🎉';
|
||
if (currentTab === 'pending') {
|
||
emptyMessage = 'Keine offenen Beiträge!';
|
||
} else if (currentTab === 'expired') {
|
||
emptyMessage = 'Keine abgelaufenen oder abgeschlossenen Beiträge.';
|
||
}
|
||
|
||
if (searchActive) {
|
||
emptyMessage = 'Keine Beiträge gefunden.';
|
||
emptyIcon = '🔍';
|
||
}
|
||
|
||
container.innerHTML = `${summaryHtml}
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">${emptyIcon}</div>
|
||
<div class="empty-state-text">
|
||
${emptyMessage}
|
||
</div>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `${summaryHtml}${visibleItems
|
||
.map(({ post, status }, index) => createPostCard(post, status, {
|
||
index,
|
||
globalIndex: index + 1,
|
||
totalFiltered: filteredItems.length,
|
||
totalOverall: posts.length,
|
||
tabTotalCount,
|
||
searchActive
|
||
}))
|
||
.join('')}`;
|
||
|
||
visibleItems.forEach(({ post, status }) => attachPostEventHandlers(post, status));
|
||
|
||
if (visibleCount < filteredItems.length) {
|
||
const loadMoreContainer = document.createElement('div');
|
||
loadMoreContainer.className = 'posts-load-more';
|
||
|
||
const loadMoreButton = document.createElement('button');
|
||
loadMoreButton.type = 'button';
|
||
loadMoreButton.className = 'btn btn-secondary posts-load-more__btn';
|
||
loadMoreButton.textContent = 'Weitere Beiträge laden';
|
||
loadMoreButton.addEventListener('click', () => {
|
||
loadMoreButton.disabled = true;
|
||
loadMoreButton.textContent = 'Lade...';
|
||
loadMorePosts(currentTab, { triggeredByScroll: false });
|
||
});
|
||
|
||
loadMoreContainer.appendChild(loadMoreButton);
|
||
container.appendChild(loadMoreContainer);
|
||
|
||
observeLoadMoreElement(loadMoreContainer, currentTab);
|
||
}
|
||
|
||
if (!focusHandled && focusTargetInfo.index !== -1 && focusTargetInfo.post) {
|
||
requestAnimationFrame(() => highlightPostCard(focusTargetInfo.post));
|
||
}
|
||
}
|
||
|
||
function attachPostEventHandlers(post, status) {
|
||
const card = document.getElementById(`post-${post.id}`);
|
||
if (!card) {
|
||
return;
|
||
}
|
||
|
||
const openBtn = card.querySelector('.btn-open');
|
||
if (openBtn) {
|
||
openBtn.addEventListener('click', () => openPost(post.id));
|
||
}
|
||
|
||
const deleteBtn = card.querySelector('.btn-delete');
|
||
if (deleteBtn) {
|
||
deleteBtn.addEventListener('click', () => deletePost(post.id));
|
||
}
|
||
|
||
const screenshotEl = card.querySelector('.post-screenshot');
|
||
if (screenshotEl && screenshotEl.dataset.screenshot) {
|
||
const url = screenshotEl.dataset.screenshot;
|
||
const openHandler = () => openScreenshotModal(url);
|
||
screenshotEl.addEventListener('click', openHandler);
|
||
screenshotEl.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Enter' || event.key === ' ') {
|
||
event.preventDefault();
|
||
openScreenshotModal(url);
|
||
}
|
||
});
|
||
}
|
||
|
||
const toggleButtons = card.querySelectorAll('.profile-line__toggle');
|
||
toggleButtons.forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
const profileNumber = parseInt(button.dataset.profile, 10);
|
||
const currentStatus = button.dataset.status || 'pending';
|
||
toggleProfileStatus(post.id, profileNumber, currentStatus);
|
||
});
|
||
});
|
||
|
||
const editPostBtn = card.querySelector('.btn-edit-post');
|
||
if (editPostBtn) {
|
||
editPostBtn.addEventListener('click', () => {
|
||
openManualPostModal({ mode: 'edit', post });
|
||
});
|
||
}
|
||
|
||
const deadlineButton = card.querySelector('.post-deadline__calendar');
|
||
if (deadlineButton) {
|
||
deadlineButton.addEventListener('click', (event) => {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
openNativeDeadlinePicker(post, deadlineButton);
|
||
});
|
||
}
|
||
|
||
const clearDeadlineButton = card.querySelector('.post-deadline__clear');
|
||
if (clearDeadlineButton) {
|
||
clearDeadlineButton.addEventListener('click', (event) => {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
clearDeadline(post.id);
|
||
});
|
||
}
|
||
|
||
const successCheckbox = card.querySelector('.success-checkbox-input');
|
||
if (successCheckbox) {
|
||
successCheckbox.addEventListener('change', async () => {
|
||
await toggleSuccessStatus(post.id, successCheckbox.checked);
|
||
});
|
||
}
|
||
|
||
const targetSelect = card.querySelector('.post-target__select');
|
||
if (targetSelect) {
|
||
targetSelect.dataset.originalValue = String(status.targetCount);
|
||
targetSelect.addEventListener('change', () => {
|
||
const value = parseInt(targetSelect.value, 10);
|
||
if (targetSelect.dataset.originalValue && String(value) === targetSelect.dataset.originalValue) {
|
||
targetSelect.blur();
|
||
return;
|
||
}
|
||
updateTargetInline(post.id, value, targetSelect);
|
||
targetSelect.blur();
|
||
});
|
||
}
|
||
}
|
||
|
||
function openScreenshotModal(url) {
|
||
if (!screenshotModal || !url) {
|
||
return;
|
||
}
|
||
|
||
screenshotModalLastFocus = document.activeElement;
|
||
screenshotModalImage.src = url;
|
||
resetScreenshotZoom();
|
||
screenshotModalPreviousOverflow = document.body.style.overflow;
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
screenshotModal.removeAttribute('hidden');
|
||
screenshotModal.classList.add('open');
|
||
|
||
if (screenshotModalClose) {
|
||
screenshotModalClose.focus();
|
||
}
|
||
|
||
if (screenshotModalImage.complete) {
|
||
applyScreenshotModalSize();
|
||
} else {
|
||
const handleLoad = () => {
|
||
applyScreenshotModalSize();
|
||
};
|
||
screenshotModalImage.addEventListener('load', handleLoad, { once: true });
|
||
}
|
||
}
|
||
|
||
function closeScreenshotModal() {
|
||
if (!screenshotModal) {
|
||
return;
|
||
}
|
||
|
||
if (!screenshotModal.classList.contains('open')) {
|
||
return;
|
||
}
|
||
|
||
resetScreenshotZoom();
|
||
screenshotModal.classList.remove('open');
|
||
screenshotModal.setAttribute('hidden', '');
|
||
screenshotModalImage.src = '';
|
||
document.body.style.overflow = screenshotModalPreviousOverflow;
|
||
|
||
if (screenshotModalLastFocus && typeof screenshotModalLastFocus.focus === 'function') {
|
||
screenshotModalLastFocus.focus();
|
||
}
|
||
}
|
||
|
||
function resetScreenshotZoom() {
|
||
screenshotModalZoomed = false;
|
||
if (screenshotModalContent) {
|
||
screenshotModalContent.classList.remove('zoomed');
|
||
screenshotModalContent.style.width = '';
|
||
screenshotModalContent.style.height = '';
|
||
screenshotModalContent.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
||
}
|
||
if (screenshotModalImage) {
|
||
screenshotModalImage.classList.remove('zoomed');
|
||
}
|
||
applyScreenshotModalSize();
|
||
}
|
||
|
||
function toggleScreenshotZoom() {
|
||
if (!screenshotModalContent || !screenshotModalImage) {
|
||
return;
|
||
}
|
||
|
||
screenshotModalZoomed = !screenshotModalZoomed;
|
||
screenshotModalContent.classList.toggle('zoomed', screenshotModalZoomed);
|
||
screenshotModalImage.classList.toggle('zoomed', screenshotModalZoomed);
|
||
|
||
if (screenshotModalZoomed) {
|
||
screenshotModalContent.style.width = 'min(95vw, 1300px)';
|
||
screenshotModalContent.style.height = '92vh';
|
||
screenshotModalContent.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
||
} else {
|
||
screenshotModalContent.style.width = '';
|
||
screenshotModalContent.style.height = '';
|
||
applyScreenshotModalSize();
|
||
}
|
||
}
|
||
|
||
// Create post card HTML
|
||
function createPostCard(post, status, meta = {}) {
|
||
const createdDate = formatDateTime(post.created_at) || '—';
|
||
const lastChangeDate = formatDateTime(post.last_change || post.created_at) || '—';
|
||
|
||
const resolvedScreenshotPath = post.screenshot_path
|
||
? (post.screenshot_path.startsWith('http')
|
||
? post.screenshot_path
|
||
: `${API_URL.replace(/\/api$/, '')}${post.screenshot_path.startsWith('/') ? '' : '/'}${post.screenshot_path}`)
|
||
: `${API_URL}/posts/${post.id}/screenshot`;
|
||
|
||
const screenshotHtml = `
|
||
<div class="post-screenshot" data-screenshot="${escapeHtml(resolvedScreenshotPath)}" role="button" tabindex="0" aria-label="Screenshot anzeigen">
|
||
<img src="${escapeHtml(resolvedScreenshotPath)}" alt="Screenshot zum Beitrag" loading="lazy" />
|
||
</div>
|
||
`;
|
||
|
||
const displayIndex = typeof meta.globalIndex === 'number' ? meta.globalIndex : typeof meta.index === 'number' ? meta.index + 1 : null;
|
||
const totalFiltered = typeof meta.totalFiltered === 'number' ? meta.totalFiltered : posts.length;
|
||
const totalOverall = typeof meta.totalOverall === 'number' ? meta.totalOverall : posts.length;
|
||
const tabTotalCount = typeof meta.tabTotalCount === 'number' ? meta.tabTotalCount : posts.length;
|
||
const searchActive = !!meta.searchActive;
|
||
|
||
const counterBadge = displayIndex !== null
|
||
? `
|
||
<div class="post-counter" aria-hidden="true">
|
||
<span class="post-counter__value">${String(displayIndex).padStart(2, '0')}</span>
|
||
</div>
|
||
`
|
||
: '';
|
||
|
||
const profileRowsHtml = status.profileStatuses.map((profileStatus) => {
|
||
const classes = ['profile-line', `profile-line--${profileStatus.status}`];
|
||
const isCurrentProfile = parseInt(profileStatus.profile_number, 10) === status.profileNumber;
|
||
if (isCurrentProfile) {
|
||
classes.push('profile-line--current');
|
||
}
|
||
let label = 'Wartet';
|
||
if (profileStatus.status === 'done') {
|
||
const doneDate = formatDateTime(profileStatus.checked_at);
|
||
label = doneDate ? `Erledigt (${doneDate})` : 'Erledigt';
|
||
} else if (profileStatus.status === 'available') {
|
||
label = 'Bereit';
|
||
}
|
||
|
||
const toggleLabel = profileStatus.status === 'done'
|
||
? 'Als offen markieren'
|
||
: 'Als erledigt markieren';
|
||
|
||
// Disable toggle button if post is expired
|
||
const toggleDisabled = status.isExpired ? 'disabled' : '';
|
||
|
||
const badgeHtml = isCurrentProfile ? '<span class="profile-line__badge">Dein Profil</span>' : '';
|
||
|
||
return `
|
||
<div class="${classes.join(' ')}">
|
||
<span class="profile-line__name">${escapeHtml(profileStatus.profile_name)}${badgeHtml}</span>
|
||
<span class="profile-line__status">${escapeHtml(label)}</span>
|
||
<div class="profile-line__actions">
|
||
<button type="button" class="profile-line__toggle" data-post-id="${post.id}" data-profile="${profileStatus.profile_number}" data-status="${profileStatus.status}" ${toggleDisabled}>
|
||
${escapeHtml(toggleLabel)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
const infoMessages = [];
|
||
if (status.isExpired) {
|
||
infoMessages.push('Deadline ist abgelaufen.');
|
||
}
|
||
if (!status.isCurrentProfileRequired) {
|
||
infoMessages.push('Dieses Profil muss den Beitrag nicht bestätigen.');
|
||
} else if (status.isCurrentProfileDone) {
|
||
infoMessages.push('Für dein Profil erledigt.');
|
||
} else if (status.waitingForNames.length) {
|
||
infoMessages.push(`Wartet auf: ${status.waitingForNames.join(', ')}`);
|
||
}
|
||
|
||
const infoHtml = infoMessages.length
|
||
? `
|
||
<div class="post-hints">
|
||
${infoMessages.map((message) => `
|
||
<div class="post-hint${message.includes('erledigt') ? ' post-hint--success' : ''}">
|
||
${escapeHtml(message)}
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`
|
||
: '';
|
||
|
||
const directLinkHtml = post.url
|
||
? `
|
||
<div class="post-link">
|
||
<span class="post-link__label">Direktlink:</span>
|
||
<a href="${escapeHtml(post.url)}" target="_blank" rel="noopener noreferrer" class="post-link__anchor">
|
||
${escapeHtml(formatUrlForDisplay(post.url))}
|
||
</a>
|
||
</div>
|
||
`
|
||
: '';
|
||
|
||
const openButtonHtml = (status.canCurrentProfileCheck && !status.isExpired)
|
||
? `
|
||
<button class="btn btn-success btn-open">Beitrag öffnen & abhaken</button>
|
||
`
|
||
: '';
|
||
|
||
const bodyClasses = ['post-body'];
|
||
if (resolvedScreenshotPath) {
|
||
bodyClasses.push('post-body--with-screenshot');
|
||
}
|
||
|
||
let creatorName = typeof post.created_by_name === 'string' && post.created_by_name.trim()
|
||
? post.created_by_name.trim()
|
||
: null;
|
||
|
||
// Remove ", Story ansehen" suffix if present
|
||
if (creatorName && creatorName.endsWith(', Story ansehen')) {
|
||
creatorName = creatorName.slice(0, -16).trim();
|
||
}
|
||
const creatorDisplay = creatorName || 'Unbekannt';
|
||
|
||
const titleText = (post.title && post.title.trim()) ? post.title.trim() : creatorDisplay;
|
||
|
||
const deadlineText = formatDeadline(post.deadline_at);
|
||
const hasDeadline = Boolean(post.deadline_at);
|
||
const isOverdue = hasDeadline && (new Date(post.deadline_at).getTime() < Date.now());
|
||
const deadlineClasses = ['post-deadline'];
|
||
|
||
let deadlineStyle = '';
|
||
if (hasDeadline) {
|
||
deadlineClasses.push('has-deadline');
|
||
|
||
// Calculate color based on time until deadline (smooth gradient)
|
||
const now = Date.now();
|
||
const deadlineTime = new Date(post.deadline_at).getTime();
|
||
const hoursUntilDeadline = (deadlineTime - now) / (1000 * 60 * 60);
|
||
|
||
let color;
|
||
if (hoursUntilDeadline < 0) {
|
||
// Overdue - dark red
|
||
color = '#dc2626';
|
||
} else {
|
||
// Smooth gradient from red (0h) to default gray (168h/7 days)
|
||
const maxHours = 168; // 7 days
|
||
const ratio = Math.min(hoursUntilDeadline / maxHours, 1);
|
||
|
||
// Color stops: red -> default gray (#4b5563)
|
||
// Red: rgb(220, 38, 38)
|
||
// Gray: rgb(75, 85, 99)
|
||
|
||
const r = Math.round(220 - (220 - 75) * ratio);
|
||
const g = Math.round(38 + (85 - 38) * ratio);
|
||
const b = Math.round(38 + (99 - 38) * ratio);
|
||
color = `rgb(${r}, ${g}, ${b})`;
|
||
}
|
||
|
||
deadlineStyle = `style="color: ${color};"`;
|
||
}
|
||
if (isOverdue) {
|
||
deadlineClasses.push('overdue');
|
||
}
|
||
|
||
return `
|
||
<div class="post-card ${status.isComplete ? 'complete' : ''}" id="post-${post.id}">
|
||
<div class="post-header">
|
||
${counterBadge}
|
||
<div class="post-title-with-checkbox">
|
||
<div class="post-title">${escapeHtml(titleText)}</div>
|
||
<label class="success-checkbox success-checkbox--header">
|
||
<input type="checkbox" class="success-checkbox-input" data-post-id="${post.id}" ${post.is_successful ? 'checked' : ''}>
|
||
<span>Erfolgreich</span>
|
||
</label>
|
||
</div>
|
||
<div class="post-header-right">
|
||
<div class="post-target">
|
||
<span>Benötigte Profile:</span>
|
||
<select class="control-select post-target__select" data-post-id="${post.id}">
|
||
${Array.from({ length: MAX_PROFILES }, (_, index) => index + 1)
|
||
.map((value) => `
|
||
<option value="${value}" ${value === status.targetCount ? 'selected' : ''}>${value}</option>
|
||
`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="post-status ${status.isComplete ? 'complete' : ''}">
|
||
${status.checkedCount}/${status.targetCount} ${status.isComplete ? '✓' : ''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="${bodyClasses.join(' ')}">
|
||
<div class="post-screenshot-wrapper">
|
||
${screenshotHtml}
|
||
</div>
|
||
|
||
<div class="post-content">
|
||
<div class="post-meta">
|
||
<div class="post-meta__line post-meta__line--count">
|
||
<span class="post-meta__label">Beitrag:</span>
|
||
<span class="post-meta__value">#${displayIndex !== null ? displayIndex : '-'}</span>
|
||
<span class="post-meta__stats">
|
||
(${searchActive ? `${totalFiltered} Treffer von ${tabTotalCount}` : `${tabTotalCount} im Tab`} · ${totalOverall} gesamt)
|
||
</span>
|
||
</div>
|
||
<div class="post-info">
|
||
<div>Erstellt: ${escapeHtml(createdDate)}</div>
|
||
<div>Letzte Änderung: ${escapeHtml(lastChangeDate)}</div>
|
||
</div>
|
||
<div class="post-creator">Erstellt von: ${escapeHtml(creatorDisplay)}</div>
|
||
</div>
|
||
${directLinkHtml}
|
||
|
||
<div class="post-deadline-row" data-post-id="${post.id}">
|
||
<span class="${deadlineClasses.join(' ')}" ${deadlineStyle}>Deadline: ${escapeHtml(deadlineText)}</span>
|
||
<button type="button" class="post-deadline__calendar" aria-label="Deadline bearbeiten" data-post-id="${post.id}">
|
||
📅
|
||
</button>
|
||
${hasDeadline ? `
|
||
<button type="button" class="post-deadline__clear" aria-label="Deadline entfernen" data-post-id="${post.id}">
|
||
✕
|
||
</button>
|
||
` : ''}
|
||
</div>
|
||
|
||
<div class="post-profiles">
|
||
${profileRowsHtml}
|
||
</div>
|
||
|
||
${infoHtml}
|
||
|
||
<div class="post-actions">
|
||
${openButtonHtml}
|
||
<button type="button" class="btn btn-secondary btn-edit-post">Bearbeiten</button>
|
||
${post.url ? `
|
||
<a href="${escapeHtml(post.url)}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary btn-direct-link">
|
||
Direkt öffnen
|
||
</a>
|
||
` : ''}
|
||
<button class="btn btn-danger btn-delete">Löschen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Open post and auto-check
|
||
async function openPost(postId) {
|
||
const post = posts.find((item) => item.id === postId);
|
||
if (!post) {
|
||
alert('Beitrag konnte nicht gefunden werden.');
|
||
return;
|
||
}
|
||
|
||
if (!post.url) {
|
||
alert('Für diesen Beitrag ist kein Direktlink vorhanden.');
|
||
return;
|
||
}
|
||
|
||
const status = computePostStatus(post);
|
||
|
||
if (!status.isCurrentProfileRequired) {
|
||
alert('Dieses Profil muss den Beitrag nicht bestätigen.');
|
||
return;
|
||
}
|
||
|
||
if (status.isCurrentProfileDone) {
|
||
window.open(post.url, '_blank');
|
||
return;
|
||
}
|
||
|
||
if (!status.canCurrentProfileCheck) {
|
||
if (status.waitingForNames.length) {
|
||
alert(`Wartet auf: ${status.waitingForNames.join(', ')}`);
|
||
} else {
|
||
alert('Der Beitrag kann aktuell nicht abgehakt werden.');
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await apiFetch(`${API_URL}/check-by-url`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
url: post.url,
|
||
profile_number: currentProfile
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
if (response.status === 409) {
|
||
const data = await response.json().catch(() => null);
|
||
if (data && data.error) {
|
||
alert(data.error);
|
||
return;
|
||
}
|
||
}
|
||
throw new Error('Failed to check post');
|
||
}
|
||
|
||
window.open(post.url, '_blank');
|
||
|
||
await fetchPosts({ showLoader: false });
|
||
} catch (error) {
|
||
alert('Fehler beim Abhaken des Beitrags');
|
||
console.error('Error checking post:', error);
|
||
}
|
||
}
|
||
|
||
async function toggleSuccessStatus(postId, isSuccessful) {
|
||
try {
|
||
const response = await apiFetch(`${API_URL}/posts/${postId}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ is_successful: isSuccessful })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
alert('Status konnte nicht geändert werden.');
|
||
return;
|
||
}
|
||
|
||
const updatedPost = await response.json();
|
||
posts = posts.map((item) => (item.id === postId ? updatedPost : item));
|
||
renderPosts();
|
||
} catch (error) {
|
||
console.error('Error updating success status:', error);
|
||
alert('Status konnte nicht geändert werden.');
|
||
}
|
||
}
|
||
|
||
async function toggleProfileStatus(postId, profileNumber, currentStatus) {
|
||
if (!profileNumber) {
|
||
return;
|
||
}
|
||
|
||
const desiredStatus = currentStatus === 'done' ? 'pending' : 'done';
|
||
|
||
try {
|
||
const response = await apiFetch(`${API_URL}/posts/${postId}/profile-status`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
profile_number: profileNumber,
|
||
status: desiredStatus
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const data = await response.json().catch(() => null);
|
||
const message = data && data.error ? data.error : 'Status konnte nicht geändert werden.';
|
||
alert(message);
|
||
return;
|
||
}
|
||
|
||
const updatedPost = await response.json();
|
||
posts = posts.map((item) => (item.id === postId ? updatedPost : item));
|
||
renderPosts();
|
||
} catch (error) {
|
||
console.error('Error updating profile status:', error);
|
||
alert('Profilstatus konnte nicht geändert werden.');
|
||
}
|
||
}
|
||
|
||
function populateManualPostForm(post) {
|
||
if (!manualPostForm || !post) {
|
||
return;
|
||
}
|
||
|
||
if (manualPostUrlInput) {
|
||
const normalizedUrl = typeof post.url === 'string' ? normalizeFacebookPostUrl(post.url) : null;
|
||
manualPostUrlInput.value = normalizedUrl || post.url || '';
|
||
manualPostUrlInput.disabled = true;
|
||
manualPostUrlInput.readOnly = true;
|
||
}
|
||
|
||
if (manualPostTitleInput) {
|
||
manualPostTitleInput.value = post.title || '';
|
||
}
|
||
|
||
if (manualPostTargetSelect) {
|
||
const targetValue = parseInt(post.target_count, 10);
|
||
manualPostTargetSelect.value = Number.isNaN(targetValue) ? '1' : String(targetValue);
|
||
}
|
||
|
||
if (manualPostCreatorInput) {
|
||
manualPostCreatorInput.value = post.created_by_name || '';
|
||
}
|
||
|
||
if (manualPostDeadlineInput) {
|
||
const existingValue = toDateTimeLocalValue(post.deadline_at);
|
||
manualPostDeadlineInput.value = existingValue || getDefaultDeadlineInputValue();
|
||
}
|
||
|
||
clearManualPostMessage();
|
||
}
|
||
|
||
function resetManualPostForm({ keepMessages = false } = {}) {
|
||
if (!manualPostForm) {
|
||
return;
|
||
}
|
||
|
||
manualPostMode = 'create';
|
||
manualPostEditingId = null;
|
||
|
||
manualPostForm.reset();
|
||
|
||
if (manualPostTargetSelect) {
|
||
manualPostTargetSelect.value = '1';
|
||
}
|
||
|
||
if (manualPostUrlInput) {
|
||
manualPostUrlInput.disabled = false;
|
||
manualPostUrlInput.readOnly = false;
|
||
manualPostUrlInput.value = '';
|
||
}
|
||
|
||
if (manualPostTitleInput) {
|
||
manualPostTitleInput.value = '';
|
||
}
|
||
|
||
if (manualPostCreatorInput) {
|
||
manualPostCreatorInput.value = '';
|
||
}
|
||
|
||
if (manualPostDeadlineInput) {
|
||
manualPostDeadlineInput.value = getDefaultDeadlineInputValue();
|
||
}
|
||
|
||
if (manualPostModalTitle) {
|
||
manualPostModalTitle.textContent = 'Beitrag hinzufügen';
|
||
}
|
||
|
||
if (manualPostSubmitButton) {
|
||
manualPostSubmitButton.textContent = 'Speichern';
|
||
}
|
||
|
||
if (!keepMessages) {
|
||
clearManualPostMessage();
|
||
}
|
||
}
|
||
|
||
async function saveDeadline(postId, deadlineIso) {
|
||
try {
|
||
const response = await apiFetch(`${API_URL}/posts/${postId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ deadline_at: deadlineIso })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const data = await response.json().catch(() => null);
|
||
const message = data && data.error ? data.error : 'Deadline konnte nicht gespeichert werden.';
|
||
alert(message);
|
||
return false;
|
||
}
|
||
|
||
const updatedPost = await response.json();
|
||
posts = posts.map((item) => (item.id === postId ? updatedPost : item));
|
||
renderPosts();
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Error updating deadline:', error);
|
||
alert('Deadline konnte nicht gespeichert werden.');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function updateTargetInline(postId, value, selectElement) {
|
||
if (!selectElement) {
|
||
return;
|
||
}
|
||
|
||
if (Number.isNaN(value) || value < 1 || value > MAX_PROFILES) {
|
||
renderPosts();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await apiFetch(`${API_URL}/posts/${postId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ target_count: value })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const data = await response.json().catch(() => null);
|
||
const message = data && data.error ? data.error : 'Anzahl konnte nicht gespeichert werden.';
|
||
alert(message);
|
||
if (selectElement.dataset.originalValue) {
|
||
selectElement.value = selectElement.dataset.originalValue;
|
||
}
|
||
renderPosts();
|
||
return;
|
||
}
|
||
|
||
const updatedPost = await response.json();
|
||
posts = posts.map((item) => (item.id === postId ? updatedPost : item));
|
||
renderPosts();
|
||
} catch (error) {
|
||
console.error('Error updating target count:', error);
|
||
alert('Anzahl konnte nicht gespeichert werden.');
|
||
if (selectElement.dataset.originalValue) {
|
||
selectElement.value = selectElement.dataset.originalValue;
|
||
}
|
||
renderPosts();
|
||
}
|
||
}
|
||
|
||
async function handleManualPostSubmit(event) {
|
||
event.preventDefault();
|
||
|
||
if (!manualPostForm) {
|
||
return;
|
||
}
|
||
|
||
clearManualPostMessage();
|
||
|
||
const urlValue = manualPostUrlInput ? manualPostUrlInput.value.trim() : '';
|
||
if (!urlValue) {
|
||
displayManualPostMessage('Bitte gib einen Direktlink an.', 'error');
|
||
if (manualPostUrlInput) {
|
||
manualPostUrlInput.focus();
|
||
}
|
||
return;
|
||
}
|
||
|
||
const targetValue = manualPostTargetSelect ? manualPostTargetSelect.value : '1';
|
||
const parsedTarget = parseInt(targetValue, 10);
|
||
if (Number.isNaN(parsedTarget) || parsedTarget < 1 || parsedTarget > MAX_PROFILES) {
|
||
displayManualPostMessage('Die Anzahl der benötigten Profile muss zwischen 1 und 5 liegen.', 'error');
|
||
return;
|
||
}
|
||
|
||
const creatorValue = manualPostCreatorInput ? manualPostCreatorInput.value.trim() : '';
|
||
const deadlineValue = manualPostDeadlineInput ? manualPostDeadlineInput.value : '';
|
||
const titleValue = manualPostTitleInput ? manualPostTitleInput.value.trim() : '';
|
||
|
||
const cleanedUrl = normalizeFacebookPostUrl(urlValue);
|
||
if (!cleanedUrl) {
|
||
displayManualPostMessage('Bitte gib einen gültigen Facebook-Link an.', 'error');
|
||
if (manualPostUrlInput) {
|
||
manualPostUrlInput.focus();
|
||
manualPostUrlInput.select?.();
|
||
}
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
url: cleanedUrl,
|
||
target_count: parsedTarget
|
||
};
|
||
|
||
if (titleValue) {
|
||
payload.title = titleValue;
|
||
}
|
||
|
||
if (creatorValue) {
|
||
payload.created_by_name = creatorValue;
|
||
}
|
||
|
||
const normalizedDeadline = normalizeDeadlineInput(deadlineValue);
|
||
if (normalizedDeadline) {
|
||
payload.deadline_at = normalizedDeadline;
|
||
}
|
||
|
||
const submitButtons = manualPostForm.querySelectorAll('button, input[type="submit"]');
|
||
submitButtons.forEach((btn) => {
|
||
btn.disabled = true;
|
||
});
|
||
|
||
try {
|
||
if (manualPostMode === 'edit' && manualPostEditingId) {
|
||
const updatePayload = {
|
||
target_count: parsedTarget,
|
||
title: titleValue || ''
|
||
};
|
||
|
||
if (creatorValue || creatorValue === '') {
|
||
updatePayload.created_by_name = creatorValue || null;
|
||
}
|
||
|
||
updatePayload.deadline_at = normalizedDeadline;
|
||
|
||
const response = await apiFetch(`${API_URL}/posts/${manualPostEditingId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(updatePayload)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const data = await response.json().catch(() => null);
|
||
const message = data && data.error ? data.error : 'Beitrag konnte nicht aktualisiert werden.';
|
||
displayManualPostMessage(message, 'error');
|
||
return;
|
||
}
|
||
|
||
const updatedPost = await response.json();
|
||
posts = posts.map((item) => (item.id === manualPostEditingId ? updatedPost : item));
|
||
renderPosts();
|
||
closeManualPostModal();
|
||
} else {
|
||
const response = await apiFetch(`${API_URL}/posts`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const data = await response.json().catch(() => null);
|
||
if (response.status === 409 && data && data.error) {
|
||
displayManualPostMessage(data.error, 'error');
|
||
} else {
|
||
const message = data && data.error ? data.error : 'Beitrag konnte nicht erstellt werden.';
|
||
displayManualPostMessage(message, 'error');
|
||
}
|
||
return;
|
||
}
|
||
|
||
const createdPost = await response.json();
|
||
posts = [createdPost, ...posts.filter((item) => item.id !== createdPost.id)];
|
||
renderPosts();
|
||
displayManualPostMessage('Beitrag wurde erstellt.', 'success');
|
||
resetManualPostForm({ keepMessages: true });
|
||
if (manualPostUrlInput) {
|
||
manualPostUrlInput.focus();
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating manual post:', error);
|
||
displayManualPostMessage('Beitrag konnte nicht erstellt werden.', 'error');
|
||
} finally {
|
||
submitButtons.forEach((btn) => {
|
||
btn.disabled = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
// Delete post
|
||
async function deletePost(postId) {
|
||
if (!confirm('Beitrag wirklich löschen?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await apiFetch(`${API_URL}/posts/${postId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to delete post');
|
||
}
|
||
|
||
await fetchPosts({ showLoader: false });
|
||
} catch (error) {
|
||
alert('Fehler beim Löschen des Beitrags');
|
||
console.error('Error deleting post:', error);
|
||
}
|
||
}
|
||
|
||
// Utility functions
|
||
function showLoading() {
|
||
document.getElementById('loading').style.display = 'block';
|
||
document.getElementById('postsContainer').style.display = 'none';
|
||
}
|
||
|
||
function hideLoading() {
|
||
document.getElementById('loading').style.display = 'none';
|
||
document.getElementById('postsContainer').style.display = 'block';
|
||
}
|
||
|
||
function showError(message) {
|
||
const errorEl = document.getElementById('error');
|
||
errorEl.textContent = message;
|
||
errorEl.style.display = 'block';
|
||
}
|
||
|
||
function hideError() {
|
||
document.getElementById('error').style.display = 'none';
|
||
}
|
||
|
||
function escapeHtml(unsafe) {
|
||
if (unsafe === null || unsafe === undefined) {
|
||
unsafe = '';
|
||
}
|
||
|
||
if (typeof unsafe !== 'string') {
|
||
unsafe = String(unsafe);
|
||
}
|
||
|
||
return unsafe
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// Auto-check on page load if URL parameter is present
|
||
function checkAutoCheck() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const autoCheckUrl = urlParams.get('check');
|
||
|
||
if (autoCheckUrl) {
|
||
// Try to check this URL automatically
|
||
apiFetch(`${API_URL}/check-by-url`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
url: decodeURIComponent(autoCheckUrl),
|
||
profile_number: currentProfile
|
||
})
|
||
}).then(() => {
|
||
// Remove nur den check-Parameter aus der URL
|
||
try {
|
||
const url = new URL(window.location.href);
|
||
url.searchParams.delete('check');
|
||
const paramsString = url.searchParams.toString();
|
||
const newUrl = paramsString ? `${url.pathname}?${paramsString}${url.hash}` : `${url.pathname}${url.hash}`;
|
||
window.history.replaceState({}, document.title, newUrl);
|
||
} catch (error) {
|
||
console.warn('Konnte check-Parameter nicht entfernen:', error);
|
||
}
|
||
fetchPosts({ showLoader: false });
|
||
}).catch(console.error);
|
||
}
|
||
}
|
||
|
||
if (screenshotModalClose) {
|
||
screenshotModalClose.addEventListener('click', closeScreenshotModal);
|
||
}
|
||
|
||
if (screenshotModalBackdrop) {
|
||
screenshotModalBackdrop.addEventListener('click', closeScreenshotModal);
|
||
}
|
||
|
||
if (screenshotModalImage) {
|
||
screenshotModalImage.addEventListener('click', (event) => {
|
||
event.stopPropagation();
|
||
toggleScreenshotZoom();
|
||
});
|
||
|
||
screenshotModalImage.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Enter' || event.key === ' ') {
|
||
event.preventDefault();
|
||
toggleScreenshotZoom();
|
||
}
|
||
});
|
||
|
||
screenshotModalImage.setAttribute('tabindex', '0');
|
||
screenshotModalImage.setAttribute('role', 'button');
|
||
screenshotModalImage.setAttribute('aria-label', 'Screenshot vergrößern oder verkleinern');
|
||
}
|
||
|
||
document.addEventListener('keydown', (event) => {
|
||
if (event.key !== 'Escape') {
|
||
return;
|
||
}
|
||
|
||
if (manualPostModal && manualPostModal.classList.contains('open')) {
|
||
event.preventDefault();
|
||
closeManualPostModal();
|
||
return;
|
||
}
|
||
|
||
if (bookmarkPanelVisible) {
|
||
event.preventDefault();
|
||
toggleBookmarkPanel(false);
|
||
return;
|
||
}
|
||
|
||
if (screenshotModal && screenshotModal.classList.contains('open')) {
|
||
if (screenshotModalZoomed) {
|
||
resetScreenshotZoom();
|
||
return;
|
||
}
|
||
closeScreenshotModal();
|
||
}
|
||
});
|
||
|
||
window.addEventListener('resize', () => {
|
||
if (screenshotModal && screenshotModal.classList.contains('open') && !screenshotModalZoomed) {
|
||
applyScreenshotModalSize();
|
||
}
|
||
});
|
||
|
||
// Initialize
|
||
initializeBookmarks();
|
||
loadAutoRefreshSettings();
|
||
initializeFocusParams();
|
||
initializeTabFromUrl();
|
||
loadSortMode();
|
||
resetManualPostForm();
|
||
loadProfile();
|
||
startProfilePolling();
|
||
fetchPosts();
|
||
checkAutoCheck();
|
||
applyAutoRefreshSettings();
|