// ============================================================ // EVENTS EDITOR // ============================================================ const SITE_LANGUAGES = ; const DEFAULT_LANG = ''; let eventsData = null; let eventsLoaded = false; let currentEventIndex = null; const EVENT_TRANSLATABLE = ['title', 'location', 'description', 'admission']; function adminLangCodes() { return Object.keys(SITE_LANGUAGES || {}); } function adminLangLabel(code) { return String((SITE_LANGUAGES && SITE_LANGUAGES[code]) || code).toUpperCase(); } function activeAdminLang(section) { return section.querySelector('.ce-lang-tab.active')?.dataset.lang || DEFAULT_LANG; } function readAdminLangField(fieldEl) { const input = fieldEl.querySelector('textarea, input:not([type="hidden"]), select'); return input ? input.value || '' : ''; } function writeAdminLangField(fieldEl, value) { const input = fieldEl.querySelector('textarea, input:not([type="hidden"]), select'); if (!input) return; input.value = value == null ? '' : String(value); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } async function translateAdminLanguageSection(section, button) { if (!AI_FEATURES_ENABLED || !section) return; const sourceLang = activeAdminLang(section); const targetLangs = adminLangCodes().filter(code => code !== sourceLang); const fields = {}; section.querySelectorAll('[data-lang="' + CSS.escape(sourceLang) + '"][data-i18n-field]').forEach(fieldEl => { const value = readAdminLangField(fieldEl); if (value.trim() !== '') fields[fieldEl.dataset.i18nField] = value; }); if (!Object.keys(fields).length) { showToast(t('editor.translate_no_source') || 'No source text to translate.', 'error'); return; } const previous = button.textContent; button.disabled = true; button.textContent = t('editor.translating') || 'Translating...'; try { const formData = new FormData(); formData.append('action', 'ai-generate-text'); formData.append('maxOutputTokens', '1800'); formData.append('prompt', [ 'Translate these raiju admin editor fields from ' + sourceLang + ' into the requested target languages.', 'Return strict JSON only. Shape: {"translations":{"LANG":{"field":"translated value"}}}.', 'Keep HTML tags if present. Do not add Markdown or explanations.', '', JSON.stringify({ sourceLang, targetLangs, fields: fields }, null, 2) ].join('\n')); formData.append('csrf_token', CSRF_TOKEN); const response = await fetch('api.php', { method: 'POST', body: formData }); const result = await response.json(); if (!result.success) throw new Error(result.message || t('toast.error')); let payload; try { payload = JSON.parse(String(result.data?.text || '').replace(/^```json\s*|\s*```$/g, '').trim()); } catch (error) { throw new Error(t('editor.translate_invalid_response') || 'AI returned an unreadable translation.'); } const translations = payload.translations || payload; targetLangs.forEach(lang => { Object.entries(translations[lang] || {}).forEach(([field, value]) => { const fieldEl = section.querySelector('[data-lang="' + CSS.escape(lang) + '"][data-i18n-field="' + CSS.escape(field) + '"]'); if (fieldEl) writeAdminLangField(fieldEl, value); }); }); updateAiUsage(result.data ? result.data.limits : null); showToast(t('editor.translate_done') || 'Translations inserted.', 'success'); } catch (error) { showToast(error.message, 'error'); } finally { button.disabled = false; button.textContent = previous; } } async function loadEventsEditor() { try { const response = await fetch('api.php?action=load-events'); const result = await response.json(); if (result.success) { eventsData = result.data; renderEventsList(); } else { showToast(result.message, 'error'); } } catch (error) { showToast(t('toast.error_loading_events', {message: error.message}), 'error'); } } function renderEventsList() { const tbody = document.getElementById('eventsListBody'); if (!tbody) return; tbody.innerHTML = ''; if (eventsData && eventsData.lastModified) { const d = new Date(eventsData.lastModified); document.getElementById('eventsLastModified').textContent = t('editor.last_saved', {date: formatDateShort(d)}); } else { document.getElementById('eventsLastModified').textContent = ''; } const events = (eventsData && eventsData.events) || []; if (events.length === 0) { rjRenderEmptyRow(tbody, 4, t('events.no_events'), { label: t('events.new_event'), onClick: addNewEvent }); renderAdminListFooter('eventsListFooter', 'events', 0, getDashboardPageSize(), renderEventsList, 'eventsListFooterTop'); return; } // Sort by date ascending events.sort((a, b) => (a.date || '').localeCompare(b.date || '')); const pageSize = getDashboardPageSize(); const paged = pageSlice(events, 'events', pageSize); renderAdminListFooter('eventsListFooter', 'events', events.length, pageSize, renderEventsList, 'eventsListFooterTop'); const todayStr = new Date().toISOString().split('T')[0]; paged.items.forEach((event) => { const index = events.indexOf(event); const title = event.title?.[DEFAULT_LANG] || event.title?.en || event.title?.de || t('events.untitled'); const dateStr = event.date || ''; const endDateStr = event['end-date'] || dateStr; // Status — upcoming / today / past let statusKey, statusClass; if (!dateStr) { statusKey = 'events.status_draft'; statusClass = 'events-status events-status--draft'; } else if (dateStr === todayStr || (dateStr <= todayStr && endDateStr >= todayStr)) { statusKey = 'events.status_today'; statusClass = 'events-status events-status--today'; } else if (dateStr > todayStr) { statusKey = 'events.status_upcoming'; statusClass = 'events-status events-status--upcoming'; } else { statusKey = 'events.status_past'; statusClass = 'events-status events-status--past'; } const tr = document.createElement('tr'); tr.className = 'page-list-row'; const tdTitle = document.createElement('td'); tdTitle.className = 'page-list-cell-title'; const titleLink = document.createElement('a'); titleLink.href = '#'; titleLink.className = 'page-list-title-link'; titleLink.textContent = title; titleLink.onclick = (e) => { e.preventDefault(); openEventEditor(index); }; tdTitle.appendChild(titleLink); // Hover action row 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('pages.edit'); editLink.onclick = (e) => { e.preventDefault(); openEventEditor(index); }; actions.appendChild(editLink); if (event.url) { const sep1 = document.createElement('span'); sep1.className = 'page-list-row-action-sep'; sep1.textContent = '|'; actions.appendChild(sep1); const viewLink = document.createElement('a'); viewLink.href = event.url; viewLink.target = '_blank'; viewLink.rel = 'noopener'; viewLink.className = 'page-list-row-action'; viewLink.innerHTML = icon('eye', 12, '2') + ' ' + t('pages.view'); actions.appendChild(viewLink); } const sep2 = document.createElement('span'); sep2.className = 'page-list-row-action-sep'; sep2.textContent = '|'; actions.appendChild(sep2); const dupLink = document.createElement('a'); dupLink.href = '#'; dupLink.className = 'page-list-row-action'; dupLink.innerHTML = icon('duplicate', 12, '2') + ' ' + t('pages.duplicate'); dupLink.onclick = (e) => { e.preventDefault(); duplicateEvent(index); }; actions.appendChild(dupLink); const sep3 = document.createElement('span'); sep3.className = 'page-list-row-action-sep'; sep3.textContent = '|'; actions.appendChild(sep3); const trashLink = document.createElement('a'); trashLink.href = '#'; trashLink.className = 'page-list-row-action page-list-row-action--danger'; trashLink.innerHTML = icon('trash', 12, '2') + ' ' + t('pages.trash'); trashLink.onclick = (e) => { e.preventDefault(); deleteEventDashboard(index); }; actions.appendChild(trashLink); tdTitle.appendChild(actions); const tdDate = document.createElement('td'); tdDate.className = 'page-list-cell-date'; tdDate.textContent = dateStr ? (window.rjFormatDate ? rjFormatDate(dateStr) : dateStr) : '—'; const tdLocation = document.createElement('td'); tdLocation.textContent = event.location?.[DEFAULT_LANG] || event.location?.en || event.location?.de || ''; const tdStatus = document.createElement('td'); tdStatus.className = 'page-list-cell-date'; const statusBadge = document.createElement('span'); statusBadge.className = statusClass; statusBadge.textContent = t(statusKey); tdStatus.appendChild(statusBadge); tr.appendChild(tdTitle); tr.appendChild(tdDate); tr.appendChild(tdLocation); tr.appendChild(tdStatus); tbody.appendChild(tr); }); } function openEventEditor(index) { currentEventIndex = index; document.getElementById('eventsListView').style.display = 'none'; document.getElementById('eventsEditorView').style.display = ''; const event = eventsData.events[index]; const title = event.title?.[DEFAULT_LANG] || event.title?.en || t('events.untitled'); document.getElementById('eventEditorTitle').textContent = title; document.getElementById('eventEditorDeleteBtn').style.display = event.id ? '' : 'none'; const body = document.getElementById('eventEditorBody'); body.innerHTML = ''; renderEventFields(body, event, index); } function closeEventEditor() { document.getElementById('eventsEditorView').style.display = 'none'; document.getElementById('eventsListView').style.display = ''; currentEventIndex = null; } function saveCurrentEvent() { if (currentEventIndex === null) return; saveEventDashboard(currentEventIndex); } function deleteCurrentEvent() { if (currentEventIndex === null) return; deleteEventDashboard(currentEventIndex); } function duplicateEvent(index) { const src = eventsData.events[index]; const copy = JSON.parse(JSON.stringify(src)); copy.id = ''; // unsaved → server assigns on save eventsData.events.push(copy); renderEventsList(); openEventEditor(eventsData.events.length - 1); } function renderEventFields(container, eventObj, index) { const prefix = `events.${index}`; // Date/time row const dateRow = document.createElement('div'); dateRow.className = 'ce-field-row'; dateRow.innerHTML = `
`; container.appendChild(dateRow); // URL const urlField = document.createElement('div'); urlField.className = 'ce-field'; urlField.innerHTML = ` `; container.appendChild(urlField); // Image const imgField = document.createElement('div'); imgField.className = 'ce-field'; imgField.innerHTML = `
`; const eventBrowseBtn = imgField.querySelector('.ce-browse-btn'); const eventImgInput = imgField.querySelector('.ce-input'); eventBrowseBtn.addEventListener('click', function() { browseImageForField(eventImgInput, null); }); container.appendChild(imgField); // Translatable fields — language tabs const langSection = document.createElement('div'); langSection.className = 'ce-lang-section'; const langCodes = Object.keys(SITE_LANGUAGES); const isMultiLang = langCodes.length > 1; const tabsHtml = isMultiLang ? langCodes.map(code => { const isDefault = code === DEFAULT_LANG; return ``; }).join('') : ''; langSection.innerHTML = isMultiLang ? `
${tabsHtml}
${AI_FEATURES_ENABLED ? `` : ''}
` : ''; langCodes.forEach(code => { const panel = document.createElement('div'); panel.className = 'ce-lang-panel'; if (code === DEFAULT_LANG) panel.classList.add('active'); panel.setAttribute('role', 'tabpanel'); panel.dataset.lang = code; panel.dataset.eventIdx = index; panel.hidden = isMultiLang && code !== DEFAULT_LANG; EVENT_TRANSLATABLE.forEach(field => { const val = eventObj[field]?.[code] || ''; const isLong = field === 'description'; const fieldDiv = document.createElement('div'); fieldDiv.className = 'ce-field'; fieldDiv.dataset.lang = code; fieldDiv.dataset.i18nField = field; fieldDiv.innerHTML = ``; if (isLong) { const ta = document.createElement('textarea'); ta.className = 'ce-textarea'; ta.value = val; ta.dataset.eventPath = `${prefix}.${field}.${code}`; ta.rows = 3; fieldDiv.appendChild(ta); } else { const input = document.createElement('input'); input.type = 'text'; input.className = 'ce-input'; input.value = val; input.dataset.eventPath = `${prefix}.${field}.${code}`; fieldDiv.appendChild(input); } panel.appendChild(fieldDiv); }); langSection.appendChild(panel); }); container.appendChild(langSection); // Tab switching langSection.querySelectorAll('.ce-lang-tab').forEach((tab, tabIndex) => { const activateTab = (targetTab, focus = false) => { langSection.querySelectorAll('.ce-lang-tab').forEach(t => { const active = t === targetTab; t.classList.toggle('active', active); t.setAttribute('aria-selected', active ? 'true' : 'false'); t.tabIndex = active ? 0 : -1; if (active && focus) t.focus(); }); langSection.querySelectorAll('.ce-lang-panel').forEach(p => { const active = p.dataset.lang === targetTab.dataset.lang; p.hidden = !active; p.classList.toggle('active', active); }); }; tab.addEventListener('click', () => { activateTab(tab); }); tab.addEventListener('keydown', e => { const tabs = Array.from(langSection.querySelectorAll('.ce-lang-tab')); if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return; e.preventDefault(); let next = tabIndex; if (e.key === 'ArrowLeft') next = tabIndex === 0 ? tabs.length - 1 : tabIndex - 1; if (e.key === 'ArrowRight') next = tabIndex === tabs.length - 1 ? 0 : tabIndex + 1; if (e.key === 'Home') next = 0; if (e.key === 'End') next = tabs.length - 1; activateTab(tabs[next], true); }); }); const translateBtn = langSection.querySelector('.ce-lang-translate'); if (translateBtn) { translateBtn.addEventListener('click', function() { translateAdminLanguageSection(langSection, translateBtn); }); } // Save button is in the editor header, not duplicated inline. } function collectEventData(index) { const prefix = `events.${index}`; const event = { id: eventsData.events[index].id }; // Scalar fields ['date', 'time', 'end-date', 'end-time', 'url', 'image'].forEach(field => { const el = document.querySelector(`[data-event-path="${prefix}.${field}"]`); event[field] = el ? el.value : ''; }); // Translatable fields const langCodes = Object.keys(SITE_LANGUAGES); EVENT_TRANSLATABLE.forEach(field => { event[field] = {}; langCodes.forEach(code => { const el = document.querySelector(`[data-event-path="${prefix}.${field}.${code}"]`); event[field][code] = el ? el.value : ''; }); }); return event; } async function saveEventDashboard(index) { const event = collectEventData(index); if (!event.date || !event.title[DEFAULT_LANG]) { showToast(t('toast.events_date_required'), 'error'); return; } try { const formData = new FormData(); formData.append('action', 'save-event'); formData.append('event', JSON.stringify(event)); 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(result.message || t('toast.events_saved'), 'success'); // Reload data, then return to list view const reload = await fetch('api.php?action=load-events').then(r => r.json()); if (reload.success) eventsData = reload.data; closeEventEditor(); renderEventsList(); } else { showToast(t('toast.error_generic', {message: result.message}), 'error'); } } catch (error) { showToast(t('toast.error_saving', {message: error.message}), 'error'); } } function addNewEvent() { const langCodes = Object.keys(SITE_LANGUAGES); const newEvent = { id: '', date: new Date().toISOString().split('T')[0], time: '', 'end-date': '', 'end-time': '', url: '', image: '' }; EVENT_TRANSLATABLE.forEach(field => { newEvent[field] = {}; langCodes.forEach(code => { newEvent[field][code] = ''; }); }); if (!eventsData) eventsData = { events: [], lastModified: null }; eventsData.events.push(newEvent); renderEventsList(); // Open the new event in the editor view immediately openEventEditor(eventsData.events.length - 1); } // Move event to trash (no confirm — symmetrical to Pages "move to trash") function deleteEventDashboard(index) { const event = eventsData.events[index]; if (!event) return; if (!event.id) { // New unsaved event, just remove from array eventsData.events.splice(index, 1); if (currentEventIndex !== null) closeEventEditor(); renderEventsList(); return; } const formData = new FormData(); formData.append('action', 'delete-event'); formData.append('id', event.id); formData.append('csrf_token', CSRF_TOKEN); fetch('api.php', { method: 'POST', body: formData }) .then(r => r.json()) .then(result => { if (result.success) { showToast(t('toast.event_trashed'), 'success'); if (currentEventIndex !== null) closeEventEditor(); loadEventsEditor(); } else { showToast(t('toast.error_generic', {message: result.message}), 'error'); } }) .catch(error => showToast(t('toast.error_generic', {message: error.message}), 'error')); } // ===== EVENTS TRASH ===== async function showEventsTrash() { document.getElementById('eventsListView').style.display = 'none'; document.getElementById('eventsEditorView').style.display = 'none'; document.getElementById('eventsTrashView').style.display = ''; await loadEventsTrash(); } function closeEventsTrash() { document.getElementById('eventsTrashView').style.display = 'none'; document.getElementById('eventsListView').style.display = ''; // Reload main list — restore/permanent-delete may have changed the event set loadEventsEditor(); } async function loadEventsTrash() { try { const response = await fetch('api.php?action=list-events-trash&_=' + Date.now()); const result = await response.json(); if (result.success) { renderEventsTrash(result.data); } } catch (error) { console.error('Error loading events trash:', error); } } function renderEventsTrash(items) { const tbody = document.getElementById('eventsTrashBody'); const emptyMsg = document.getElementById('eventsTrashEmptyMsg'); const emptyBtn = document.getElementById('emptyEventsTrashBtn'); const table = document.getElementById('eventsTrashTable'); tbody.innerHTML = ''; if (!items || items.length === 0) { table.style.display = 'none'; emptyMsg.style.display = 'block'; emptyBtn.style.display = 'none'; renderAdminListFooter('eventsTrashFooter', 'eventsTrash', 0, getDashboardPageSize(), function() { renderEventsTrash(items); }, 'eventsTrashFooterTop'); return; } table.style.display = ''; emptyMsg.style.display = 'none'; emptyBtn.style.display = ''; const pageSize = getDashboardPageSize(); const paged = pageSlice(items, 'eventsTrash', pageSize); renderAdminListFooter('eventsTrashFooter', 'eventsTrash', items.length, pageSize, function() { renderEventsTrash(items); }, 'eventsTrashFooterTop'); paged.items.forEach(item => { const ev = item.event || {}; const title = ev.title?.[DEFAULT_LANG] || ev.title?.en || ev.title?.de || t('events.untitled'); const tr = document.createElement('tr'); const tdTitle = document.createElement('td'); tdTitle.className = 'page-list-cell-title'; tdTitle.textContent = title; tr.appendChild(tdTitle); const tdDate = document.createElement('td'); tdDate.className = 'page-list-cell-date'; tdDate.textContent = ev.date || ''; tr.appendChild(tdDate); const tdDeletedAt = document.createElement('td'); tdDeletedAt.className = 'page-list-cell-date'; if (item.deletedAt) { const d = new Date(item.deletedAt); tdDeletedAt.textContent = rjFormatDate(d, 'datetime'); } tr.appendChild(tdDeletedAt); const tdActions = document.createElement('td'); tdActions.className = 'page-list-cell-actions'; const restoreBtn = document.createElement('button'); restoreBtn.className = 'btn btn-primary btn-sm'; restoreBtn.textContent = t('btn.restore'); restoreBtn.onclick = async () => { restoreBtn.disabled = true; restoreBtn.textContent = '...'; try { const fd = new FormData(); fd.append('action', 'restore-event'); fd.append('csrf_token', CSRF_TOKEN); fd.append('id', ev.id); const r = await fetch('api.php', { method: 'POST', body: fd }); const res = await r.json(); if (res.success) { showToast(t('toast.event_restored'), 'success'); loadEventsTrash(); } else { showToast(res.message || 'Error', 'error'); restoreBtn.disabled = false; restoreBtn.textContent = t('btn.restore'); } } catch (err) { showToast(err.message, 'error'); restoreBtn.disabled = false; restoreBtn.textContent = t('btn.restore'); } }; tdActions.appendChild(restoreBtn); const delBtn = document.createElement('button'); delBtn.className = 'btn btn-danger btn-sm'; delBtn.textContent = t('btn.delete'); delBtn.onclick = () => { showModal(t('modal.delete_permanently'), t('events.confirm_delete_permanent', {title: title}), async () => { closeModal(); try { const fd = new FormData(); fd.append('action', 'delete-event-permanent'); fd.append('csrf_token', CSRF_TOKEN); fd.append('id', ev.id); const r = await fetch('api.php', { method: 'POST', body: fd }); const res = await r.json(); if (res.success) { showToast(t('toast.event_deleted'), 'success'); loadEventsTrash(); } else { showToast(res.message || 'Error', 'error'); } } catch (err) { showToast(err.message, 'error'); } }); }; tdActions.appendChild(delBtn); tr.appendChild(tdActions); tbody.appendChild(tr); }); } async function emptyEventsTrash() { showModal(t('modal.empty_trash'), t('modal.empty_trash_confirm'), async () => { closeModal(); try { const fd = new FormData(); fd.append('action', 'empty-events-trash'); fd.append('csrf_token', CSRF_TOKEN); const r = await fetch('api.php', { method: 'POST', body: fd }); const res = await r.json(); if (res.success) { showToast(res.message || t('toast.trash_emptied'), 'success'); loadEventsTrash(); } else { showToast(res.message || 'Error', 'error'); } } catch (err) { showToast(err.message, 'error'); } }); } // Sidebar toggle (mobile) document.getElementById('sidebarToggle').addEventListener('click', () => { document.getElementById('adminSidebar').classList.toggle('open'); });