first commit
This commit is contained in:
887
web/app.js
Normal file
887
web/app.js
Normal file
@@ -0,0 +1,887 @@
|
||||
const API_URL = 'https://fb.srv.medeba-media.de/api';
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
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');
|
||||
} catch (error) {
|
||||
console.warn('Ungültiges Datum:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
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 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 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 computePostStatus(post, profileNumber = currentProfile) {
|
||||
const requiredProfiles = normalizeRequiredProfiles(post);
|
||||
const checks = normalizeChecks(post.checks);
|
||||
|
||||
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,
|
||||
completedProfilesSet,
|
||||
checkedCount,
|
||||
targetCount,
|
||||
isComplete,
|
||||
isCurrentProfileRequired,
|
||||
isCurrentProfileDone,
|
||||
canCurrentProfileCheck,
|
||||
waitingForProfiles,
|
||||
waitingForNames,
|
||||
nextRequiredProfile,
|
||||
profileNumber
|
||||
};
|
||||
}
|
||||
|
||||
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 fetch(`${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 fetch(`${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);
|
||||
}
|
||||
|
||||
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', () => {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentTab = btn.dataset.tab;
|
||||
renderPosts();
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch all posts
|
||||
async function fetchPosts() {
|
||||
try {
|
||||
showLoading();
|
||||
const response = await fetch(`${API_URL}/posts`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch posts');
|
||||
}
|
||||
|
||||
posts = await response.json();
|
||||
renderPosts();
|
||||
} catch (error) {
|
||||
showError('Fehler beim Laden der Beiträge. Stelle sicher, dass das Backend läuft.');
|
||||
console.error('Error fetching posts:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Render posts
|
||||
function renderPosts() {
|
||||
hideLoading();
|
||||
hideError();
|
||||
|
||||
const container = document.getElementById('postsContainer');
|
||||
|
||||
const postItems = posts.map((post) => ({
|
||||
post,
|
||||
status: computePostStatus(post)
|
||||
}));
|
||||
|
||||
let filteredItems = postItems;
|
||||
|
||||
if (currentTab === 'pending') {
|
||||
filteredItems = postItems.filter((item) => item.status.canCurrentProfileCheck && !item.status.isComplete);
|
||||
}
|
||||
|
||||
if (filteredItems.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🎉</div>
|
||||
<div class="empty-state-text">
|
||||
${currentTab === 'pending' ? 'Keine offenen Beiträge!' : 'Noch keine Beiträge erfasst.'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filteredItems
|
||||
.map(({ post, status }) => createPostCard(post, status))
|
||||
.join('');
|
||||
|
||||
filteredItems.forEach(({ 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 editBtn = card.querySelector('.btn-edit-target');
|
||||
if (editBtn) {
|
||||
editBtn.addEventListener('click', () => promptEditTarget(post.id, status.targetCount));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
const createdDate = formatDateTime(post.created_at) || '—';
|
||||
|
||||
const resolvedScreenshotPath = post.screenshot_path
|
||||
? (post.screenshot_path.startsWith('http')
|
||||
? post.screenshot_path
|
||||
: `${API_URL.replace(/\/api$/, '')}${post.screenshot_path}`)
|
||||
: null;
|
||||
|
||||
const screenshotHtml = resolvedScreenshotPath
|
||||
? `
|
||||
<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 profileRowsHtml = status.profileStatuses.map((profileStatus) => {
|
||||
const classes = ['profile-line', `profile-line--${profileStatus.status}`];
|
||||
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';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="${classes.join(' ')}">
|
||||
<span class="profile-line__name">${escapeHtml(profileStatus.profile_name)}</span>
|
||||
<span class="profile-line__status">${escapeHtml(label)}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const infoMessages = [];
|
||||
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
|
||||
? `
|
||||
<button class="btn btn-success btn-open">Beitrag öffnen & abhaken</button>
|
||||
`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="post-card ${status.isComplete ? 'complete' : ''}" id="post-${post.id}">
|
||||
<div class="post-header">
|
||||
<div class="post-title">${escapeHtml(post.title || '')}</div>
|
||||
<div class="post-status ${status.isComplete ? 'complete' : ''}">
|
||||
${status.checkedCount}/${status.targetCount} ${status.isComplete ? '✓' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="post-meta">
|
||||
<div class="post-info">Erstellt: ${escapeHtml(createdDate)}</div>
|
||||
<div class="post-target">
|
||||
<span>Benötigte Profile: ${status.targetCount}</span>
|
||||
<button type="button" class="btn btn-inline btn-edit-target">Anzahl ändern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${directLinkHtml}
|
||||
|
||||
<div class="post-profiles">
|
||||
${profileRowsHtml}
|
||||
</div>
|
||||
|
||||
${infoHtml}
|
||||
|
||||
${screenshotHtml}
|
||||
|
||||
<div class="post-actions">
|
||||
${openButtonHtml}
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
|
||||
// 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 fetch(`${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();
|
||||
} catch (error) {
|
||||
alert('Fehler beim Abhaken des Beitrags');
|
||||
console.error('Error checking post:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function promptEditTarget(postId, currentTarget) {
|
||||
const defaultValue = Number.isFinite(currentTarget) ? String(currentTarget) : '';
|
||||
const newValue = prompt(`Neue Anzahl (1-${MAX_PROFILES}):`, defaultValue);
|
||||
|
||||
if (newValue === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseInt(newValue, 10);
|
||||
if (Number.isNaN(parsed) || parsed < 1 || parsed > MAX_PROFILES) {
|
||||
alert(`Bitte gib eine Zahl zwischen 1 und ${MAX_PROFILES} ein.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/posts/${postId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ target_count: parsed })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => null);
|
||||
const message = data && data.error ? data.error : 'Fehler beim Aktualisieren der Anzahl.';
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPosts();
|
||||
} catch (error) {
|
||||
alert('Fehler beim Aktualisieren der Anzahl.');
|
||||
console.error('Error updating target count:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete post
|
||||
async function deletePost(postId) {
|
||||
if (!confirm('Beitrag wirklich löschen?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/posts/${postId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete post');
|
||||
}
|
||||
|
||||
await fetchPosts();
|
||||
} 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
|
||||
fetch(`${API_URL}/check-by-url`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
url: decodeURIComponent(autoCheckUrl),
|
||||
profile_number: currentProfile
|
||||
})
|
||||
}).then(() => {
|
||||
// Remove the parameter from URL
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
fetchPosts();
|
||||
}).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') {
|
||||
if (screenshotModalZoomed) {
|
||||
resetScreenshotZoom();
|
||||
return;
|
||||
}
|
||||
closeScreenshotModal();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (screenshotModal && screenshotModal.classList.contains('open') && !screenshotModalZoomed) {
|
||||
applyScreenshotModalSize();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
loadProfile();
|
||||
startProfilePolling();
|
||||
fetchPosts();
|
||||
checkAutoCheck();
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(fetchPosts, 30000);
|
||||
Reference in New Issue
Block a user