const API_URL = 'https://fb.srv.medeba-media.de/api';
// Check if we should redirect to dashboard
(function checkViewRouting() {
const params = new URLSearchParams(window.location.search);
const view = params.get('view');
if (view === 'dashboard') {
// Remove view parameter and keep other params
params.delete('view');
const remainingParams = params.toString();
window.location.href = 'dashboard.html' + (remainingParams ? '?' + remainingParams : '');
}
})();
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 REFRESH_SETTINGS_KEY = 'trackerRefreshSettings';
const SORT_SETTINGS_KEY = 'trackerSortSettings';
const SORT_SETTINGS_LEGACY_KEY = 'trackerSortMode';
const FACEBOOK_TRACKING_PARAMS = ['__cft__', '__tn__', '__eep__', 'mibextid'];
const VALID_SORT_MODES = new Set(['created', 'deadline', 'smart', 'lastCheck', 'lastChange']);
const DEFAULT_SORT_SETTINGS = { mode: 'created', direction: 'desc' };
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;
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 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(`Angezeigt: ${visibleCount} von ${filteredCount}`);
if (searchActive) {
segments.push(`Treffer: ${filteredCount} von ${tabTotalCount}`);
}
segments.push(`Tab gesamt: ${tabTotalCount}`);
segments.push(`Alle Beiträge: ${totalCountAll}`);
return `
${label}
${segments.join('·')}
`;
}
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() {
try {
const params = new URLSearchParams(window.location.search);
const tabParam = params.get('tab');
if (tabParam === 'all' || tabParam === 'pending' || tabParam === 'expired') {
currentTab = tabParam;
}
} catch (error) {
console.warn('Konnte Tab-Parameter nicht auslesen:', error);
}
updateTabButtons();
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();
if (FACEBOOK_TRACKING_PARAMS.some((trackingKey) => lowerKey.startsWith(trackingKey))) {
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 = `
`;
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
}
}
}
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;
}
// 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);
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))
);
});
}
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}
${emptyIcon}
${emptyMessage}
`;
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);
}
}
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 = `
`;
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
? `
${String(displayIndex).padStart(2, '0')}
`
: '';
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 ? 'Dein Profil' : '';
return `
${escapeHtml(profileStatus.profile_name)}${badgeHtml}
${escapeHtml(label)}
`;
}).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
? `
${infoMessages.map((message) => `
${escapeHtml(message)}
`).join('')}
`
: '';
const directLinkHtml = post.url
? `
`
: '';
const openButtonHtml = (status.canCurrentProfileCheck && !status.isExpired)
? `
`
: '';
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 `
${screenshotHtml}
Beitrag:
#${displayIndex !== null ? displayIndex : '-'}
(${searchActive ? `${totalFiltered} Treffer von ${tabTotalCount}` : `${tabTotalCount} im Tab`} · ${totalOverall} gesamt)
Erstellt: ${escapeHtml(createdDate)}
Letzte Änderung: ${escapeHtml(lastChangeDate)}
Erstellt von: ${escapeHtml(creatorDisplay)}
${directLinkHtml}
Deadline: ${escapeHtml(deadlineText)}
${hasDeadline ? `
` : ''}
${profileRowsHtml}
${infoHtml}
${openButtonHtml}
${post.url ? `
Direkt öffnen
` : ''}
`;
}
// 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, ''');
}
// 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 (screenshotModal && screenshotModal.classList.contains('open')) {
if (screenshotModalZoomed) {
resetScreenshotZoom();
return;
}
closeScreenshotModal();
}
});
window.addEventListener('resize', () => {
if (screenshotModal && screenshotModal.classList.contains('open') && !screenshotModalZoomed) {
applyScreenshotModalSize();
}
});
// Initialize
loadAutoRefreshSettings();
initializeTabFromUrl();
loadSortMode();
resetManualPostForm();
loadProfile();
startProfilePolling();
fetchPosts();
checkAutoCheck();
applyAutoRefreshSettings();