// ============================================================ // NEWS / BLOG MANAGEMENT // ============================================================ let newsLoaded = false; let newsData = []; let editingPostId = null; let newsHtmlMode = false; async function loadNews() { try { const response = await fetch('api.php?action=load-news'); const result = await response.json(); if (result.success) { newsData = result.data; renderNewsList(); } else { showToast(result.message, 'error'); } } catch (error) { showToast(t('toast.error_loading_news', {message: error.message}), 'error'); } } function renderNewsList() { const tbody = document.getElementById('newsListBody'); const defaultLang = ''; const otherLangs = ; const colCount = 2 + otherLangs.length; if (newsData.length === 0) { tbody.innerHTML = ''; rjRenderEmptyRow(tbody, colCount, t('news.no_posts'), { label: t('news.new_post'), onClick: addNewPost }); renderAdminListFooter('newsListFooter', 'news', 0, getDashboardPageSize(), renderNewsList, 'newsListFooterTop'); return; } // Group posts by slug — primary language row is the "main" entry const slugGroups = {}; newsData.forEach(post => { const slug = post.slug || post.id; if (!slugGroups[slug]) slugGroups[slug] = {}; const lang = post.lang || defaultLang; slugGroups[slug][lang] = post; }); // Sort groups by date descending (use primary lang post date, or any available) const sortedSlugs = Object.keys(slugGroups).sort((a, b) => { const aPost = slugGroups[a][defaultLang] || Object.values(slugGroups[a])[0]; const bPost = slugGroups[b][defaultLang] || Object.values(slugGroups[b])[0]; return (bPost.date || '').localeCompare(aPost.date || ''); }); tbody.innerHTML = ''; const pageSize = getDashboardPageSize(); const paged = pageSlice(sortedSlugs, 'news', pageSize); renderAdminListFooter('newsListFooter', 'news', sortedSlugs.length, pageSize, renderNewsList, 'newsListFooterTop'); paged.items.forEach(slug => { const group = slugGroups[slug]; // Primary post = default lang or first available const post = group[defaultLang] || Object.values(group)[0]; const postLang = post.lang || defaultLang; const tr = document.createElement('tr'); tr.className = 'page-list-row'; // Title cell const tdTitle = document.createElement('td'); tdTitle.className = 'page-list-cell-title'; const slugSpan = document.createElement('span'); slugSpan.className = 'page-list-slug'; slugSpan.textContent = '/news/' + slug; tdTitle.appendChild(slugSpan); const titleLink = document.createElement('a'); titleLink.href = '#'; titleLink.className = 'page-list-title-link'; titleLink.textContent = post.title || t('news.untitled'); titleLink.onclick = (e) => { e.preventDefault(); editPost(post.id); }; tdTitle.appendChild(titleLink); // Status badge if (post.hidden) { const badge = document.createElement('span'); badge.className = 'news-status news-draft'; badge.textContent = t('news.draft'); tdTitle.appendChild(badge); } // Hover actions const actions = document.createElement('div'); actions.className = 'page-list-row-actions'; const editLink = document.createElement('a'); editLink.href = '#'; editLink.className = 'page-list-row-action'; editLink.innerHTML = icon('edit', 12, '2') + ' ' + t('news.edit'); editLink.onclick = (e) => { e.preventDefault(); editPost(post.id); }; actions.appendChild(editLink); const sep1 = document.createElement('span'); sep1.className = 'page-list-row-action-sep'; sep1.textContent = '|'; actions.appendChild(sep1); const langPrefix = postLang === defaultLang ? '' : postLang + '/'; const viewLink = document.createElement('a'); viewLink.href = '../' + langPrefix + 'news/' + slug; viewLink.target = '_blank'; viewLink.className = 'page-list-row-action'; viewLink.innerHTML = icon('eye', 12, '2') + ' ' + t('news.view'); actions.appendChild(viewLink); const sep2 = document.createElement('span'); sep2.className = 'page-list-row-action-sep'; sep2.textContent = '|'; actions.appendChild(sep2); const toggleLink = document.createElement('a'); toggleLink.href = '#'; toggleLink.className = 'page-list-row-action'; toggleLink.innerHTML = post.hidden ? icon('eye', 12, '2') + ' ' + t('news.publish') : icon('eye-off', 12, '2') + ' ' + t('news.unpublish'); toggleLink.onclick = (e) => { e.preventDefault(); toggleNewsStatus(post.id); }; actions.appendChild(toggleLink); const sep3 = document.createElement('span'); sep3.className = 'page-list-row-action-sep'; sep3.textContent = '|'; actions.appendChild(sep3); const deleteLink = document.createElement('a'); deleteLink.href = '#'; deleteLink.className = 'page-list-row-action page-list-row-action--danger'; deleteLink.innerHTML = icon('trash', 12, '2') + ' ' + t('btn.delete'); deleteLink.onclick = (e) => { e.preventDefault(); deletePost(post.id); }; actions.appendChild(deleteLink); tdTitle.appendChild(actions); tr.appendChild(tdTitle); // Date cell const tdDate = document.createElement('td'); tdDate.className = 'page-list-cell-date'; if (post.date) { tdDate.textContent = rjFormatDate(post.date, 'date'); } else { tdDate.textContent = '—'; } tr.appendChild(tdDate); // Other language columns otherLangs.forEach(lang => { const td = document.createElement('td'); td.className = 'page-list-cell-lang'; const langPost = group[lang]; if (langPost) { const editBtn = document.createElement('a'); editBtn.href = '#'; editBtn.className = 'btn btn-secondary btn-sm page-list-lang-btn'; editBtn.innerHTML = icon('edit', 12) + ' ' + t('news.edit'); editBtn.onclick = (e) => { e.preventDefault(); editPost(langPost.id); }; td.appendChild(editBtn); } else { const createLink = document.createElement('a'); createLink.href = '#'; createLink.className = 'page-list-create-link'; createLink.textContent = t('pages.create'); createLink.onclick = (e) => { e.preventDefault(); createNewsTranslation(post, lang, createLink); }; td.appendChild(createLink); } tr.appendChild(td); }); tbody.appendChild(tr); }); } function showNewsList() { document.getElementById('newsListContainer').style.display = 'block'; document.getElementById('newsEditorContainer').style.display = 'none'; } function addNewPost() { const lang = ''; editingPostId = null; showPostEditor({ id: '', lang: lang, title: '', slug: '', date: new Date().toISOString().split('T')[0], author: '', excerpt: '', image: '', content: '', hidden: false }); } function editPost(postId) { const post = newsData.find(p => p.id === postId); if (!post) return; editingPostId = postId; showPostEditor(post); } function showPostEditor(post) { const isNew = !editingPostId; newsHtmlMode = false; // Hide list, show editor document.getElementById('newsListContainer').style.display = 'none'; document.getElementById('newsEditorContainer').style.display = 'block'; // Update title const editorTitle = document.getElementById('newsEditorTitle'); if (editorTitle) editorTitle.textContent = isNew ? t('news.new_post') : (post.title || t('news.edit')); // Build language options const languages = ; const langOpts = Object.entries(languages).map(([code, name]) => `` ).join(''); const container = document.getElementById('newsEditorForm'); container.innerHTML = `
${AI_FEATURES_ENABLED ? `

${t('ai.image_field_hint')}

` : ''}
`; // Set WYSIWYG content (not escaped — it's HTML) document.getElementById('newsContentWysiwyg').innerHTML = post.content || ''; // Update view + trash buttons (after form is built, so newsSlug/newsLang exist) updateNewsEditorButtons(); // Auto-generate slug from title for new posts document.getElementById('newsTitle').addEventListener('input', function() { if (!editingPostId) { const slug = this.value.toLowerCase().trim() .replace(/[äöüß]/g, m => ({ä:'ae',ö:'oe',ü:'ue',ß:'ss'})[m]) .replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); document.getElementById('newsSlug').value = slug; } }); } // WYSIWYG helpers for news editor function newsExecCmd(cmd) { document.execCommand(cmd, false, null); document.getElementById('newsContentWysiwyg').focus(); } function newsInsertLink() { showModal( t('insert_link'), '' + '', function() { const input = document.getElementById('newsLinkUrlInput'); const url = input ? input.value.trim() : ''; if (!url) return; closeModal(); document.getElementById('newsContentWysiwyg').focus(); document.execCommand('createLink', false, url); }, { html: true, confirmText: t('btn.insert'), confirmClass: 'btn btn-primary', focusSelector: '#newsLinkUrlInput' } ); setTimeout(function() { const input = document.getElementById('newsLinkUrlInput'); if (input) { input.addEventListener('keydown', function(event) { if (event.key === 'Enter') { event.preventDefault(); document.getElementById('modalConfirm').click(); } }); input.select(); } }, 0); } function newsInsertHeading() { document.execCommand('formatBlock', false, 'h3'); document.getElementById('newsContentWysiwyg').focus(); } function newsCleanHtml() { document.execCommand('removeFormat', false, null); document.getElementById('newsContentWysiwyg').focus(); } function newsToggleHtml() { const wysiwyg = document.getElementById('newsContentWysiwyg'); const html = document.getElementById('newsContentHtml'); newsHtmlMode = !newsHtmlMode; if (newsHtmlMode) { html.value = wysiwyg.innerHTML; wysiwyg.style.display = 'none'; html.style.display = 'block'; html.focus(); } else { wysiwyg.innerHTML = html.value; html.style.display = 'none'; wysiwyg.style.display = 'block'; wysiwyg.focus(); } } function updateNewsEditorButtons() { const viewBtn = document.getElementById('newsViewBtn'); const trashBtn = document.getElementById('newsTrashBtn'); if (editingPostId) { const defLang = ''; const lang = document.getElementById('newsLang')?.value || defLang; const slug = document.getElementById('newsSlug')?.value || editingPostId; const prefix = (lang === defLang) ? '../' : '../' + lang + '/'; if (viewBtn) { viewBtn.href = prefix + 'news/' + slug; viewBtn.style.display = ''; } if (trashBtn) trashBtn.style.display = ''; } else { if (viewBtn) viewBtn.style.display = 'none'; if (trashBtn) trashBtn.style.display = 'none'; } } function deleteCurrentPost() { if (!editingPostId) return; const title = document.getElementById('newsTitle')?.value || editingPostId; showModal( t('modal.delete_post'), t('modal.delete_post_confirm', { title }), async function() { closeModal(); try { const formData = new FormData(); formData.append('action', 'delete-news'); formData.append('slug', editingPostId); formData.append('csrf_token', CSRF_TOKEN); const response = await fetch('api.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { showToast(t('toast.news_deleted'), 'success'); editingPostId = null; showNewsList(); loadNews(); } else { showToast(result.message, 'error'); } } catch (e) { showToast(t('toast.error_generic', {message: e.message}), 'error'); } } ); } function getNewsContent() { if (newsHtmlMode) { return document.getElementById('newsContentHtml').value; } return document.getElementById('newsContentWysiwyg').innerHTML; } function browseNewsImage() { const inputEl = document.getElementById('newsImage'); const previewEl = document.getElementById('newsImagePreview'); browseImageForField(inputEl, previewEl); } function cancelPostEditor() { editingPostId = null; showNewsList(); renderNewsList(); } async function savePost() { const title = document.getElementById('newsTitle').value.trim(); const date = document.getElementById('newsDate').value; if (!title || !date) { showToast(t('toast.news_date_required'), 'error'); return; } const post = { id: editingPostId || '', lang: document.getElementById('newsLang').value, title: title, slug: document.getElementById('newsSlug').value.trim(), date: date, author: document.getElementById('newsAuthor').value.trim(), excerpt: document.getElementById('newsExcerpt').value.trim(), image: document.getElementById('newsImage').value.trim(), content: getNewsContent(), hidden: document.getElementById('newsHidden').checked }; try { const formData = new FormData(); formData.append('action', 'save-news'); formData.append('post', JSON.stringify(post)); formData.append('csrf_token', CSRF_TOKEN); const response = await fetch('api.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { const wasNew = !editingPostId; showToast(wasNew ? t('toast.news_created') : t('toast.news_saved'), 'success'); // Stay in editor — update slug reference for subsequent saves if (result.data?.slug) { editingPostId = result.data.slug; document.getElementById('newsSlug').value = result.data.slug; } // Update view + trash buttons updateNewsEditorButtons(); loadNews(); } else { showToast(result.message, 'error'); } } catch (error) { showToast(t('toast.error_generic', {message: error.message}), 'error'); } } function deletePost(postId) { const post = newsData.find(p => p.id === postId); const title = post ? post.title : postId; showModal(t('modal.delete_post'), t('modal.delete_post_confirm', {title: title}), async () => { try { const formData = new FormData(); formData.append('action', 'delete-news'); formData.append('post_id', postId); formData.append('csrf_token', CSRF_TOKEN); const response = await fetch('api.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { newsData = newsData.filter(p => p.id !== postId); renderNewsList(); closeModal(); showToast(t('toast.news_deleted'), 'success'); } else { showToast(result.message, 'error'); } } catch (error) { showToast(t('toast.error_generic', {message: error.message}), 'error'); } }); } async function createNewsTranslation(sourcePost, targetLang, linkEl) { linkEl.classList.add('disabled'); linkEl.textContent = '...'; try { // Clone the source post with the new language const newPost = Object.assign({}, sourcePost, { id: '', lang: targetLang, lastModified: new Date().toISOString() }); const formData = new FormData(); formData.append('action', 'save-news'); formData.append('post', JSON.stringify(newPost)); formData.append('csrf_token', CSRF_TOKEN); const response = await fetch('api.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { showToast(t('toast.news_created'), 'success'); await loadNews(); editPost(result.data.id); } else { showToast(result.message, 'error'); linkEl.classList.remove('disabled'); linkEl.textContent = t('pages.create'); } } catch (error) { showToast(t('toast.error_generic', {message: error.message}), 'error'); linkEl.classList.remove('disabled'); linkEl.textContent = t('pages.create'); } } async function toggleNewsStatus(postId) { try { const formData = new FormData(); formData.append('action', 'toggle-news-status'); formData.append('post_id', postId); formData.append('csrf_token', CSRF_TOKEN); const response = await fetch('api.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { const post = newsData.find(p => p.id === postId); if (post) post.hidden = result.data.hidden; renderNewsList(); showToast(result.data.hidden ? t('news.draft') : t('news.published'), 'success'); } else { showToast(result.message, 'error'); } } catch (error) { showToast(t('toast.error_generic', {message: error.message}), 'error'); } }