images fixing with database saves
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@ -3,370 +3,391 @@ const title = 'Admin';
|
||||
---
|
||||
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, 'Noto Sans', 'Liberation Sans', sans-serif; margin: 1rem; }
|
||||
h1, h2 { margin: 0.5rem 0; }
|
||||
section { border: 1px solid #ddd; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
|
||||
.row { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||
.events-row { display: grid; grid-template-columns: 1fr 2fr; gap: 1rem; align-items: start; }
|
||||
@media (max-width: 900px){ .events-row { grid-template-columns: 1fr; } }
|
||||
button { margin-top: 0.5rem; padding: 0.5rem 1rem; cursor: pointer; }
|
||||
.muted { color: #666; }
|
||||
.btn { display: inline-block; padding: 0.5rem 1rem; background: #222; color: #fff; border-radius: 6px; text-decoration: none; }
|
||||
.btn:hover { background: #444; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit,minmax(260px,1fr)); gap: 0.75rem; }
|
||||
.card { border: 1px solid #eee; padding: 0.75rem; border-radius: 6px; }
|
||||
label { display:block; margin-top: 0.5rem; }
|
||||
input, textarea { width: 100%; max-width: 600px; padding: 0.5rem; margin-top: 0.25rem; }
|
||||
img.thumb { max-width: 100%; height: auto; display: block; }
|
||||
.toolbar { display:flex; gap:.5rem; align-items:center; margin:.5rem 0; }
|
||||
.pill { font-size:.85rem; padding:.25rem .5rem; border:1px solid #ddd; border-radius:999px; background:#f7f7f7; }
|
||||
.dragging { opacity:.5; }
|
||||
.drag-handle { cursor:grab; padding:.25rem .5rem; border:1px dashed #bbb; border-radius:6px; font-size:.85rem; }
|
||||
.row-buttons { display:flex; gap:.5rem; margin-top:.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Admin</h1>
|
||||
<section>
|
||||
<h2>Authentifizierung</h2>
|
||||
<div id="auth-status" class="muted">Prüfe Anmeldestatus...</div>
|
||||
<div class="row">
|
||||
<a id="login-link" class="btn" href="https://cms.gallus-pub.ch/api/auth/gitea">Mit Gitea anmelden</a>
|
||||
<button id="btn-relogin">Neu anmelden</button>
|
||||
<button id="btn-logout">Abmelden</button>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, 'Noto Sans', 'Liberation Sans', sans-serif; margin: 1rem; }
|
||||
h1, h2 { margin: 0.5rem 0; }
|
||||
section { border: 1px solid #ddd; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
|
||||
.row { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||
.events-row { display: grid; grid-template-columns: 1fr 2fr; gap: 1rem; align-items: start; }
|
||||
@media (max-width: 900px){ .events-row { grid-template-columns: 1fr; } }
|
||||
button { margin-top: 0.5rem; padding: 0.5rem 1rem; cursor: pointer; }
|
||||
.muted { color: #666; }
|
||||
.btn { display: inline-block; padding: 0.5rem 1rem; background: #222; color: #fff; border-radius: 6px; text-decoration: none; }
|
||||
.btn:hover { background: #444; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit,minmax(260px,1fr)); gap: 0.75rem; }
|
||||
.card { border: 1px solid #eee; padding: 0.75rem; border-radius: 6px; }
|
||||
label { display:block; margin-top: 0.5rem; }
|
||||
input, textarea { width: 100%; max-width: 600px; padding: 0.5rem; margin-top: 0.25rem; }
|
||||
img.thumb { max-width: 100%; height: auto; display: block; }
|
||||
.toolbar { display:flex; gap:.5rem; align-items:center; margin:.5rem 0; }
|
||||
.pill { font-size:.85rem; padding:.25rem .5rem; border:1px solid #ddd; border-radius:999px; background:#f7f7f7; }
|
||||
.dragging { opacity:.5; }
|
||||
.drag-handle { cursor:grab; padding:.25rem .5rem; border:1px dashed #bbb; border-radius:6px; font-size:.85rem; }
|
||||
.row-buttons { display:flex; gap:.5rem; margin-top:.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Admin</h1>
|
||||
<section>
|
||||
<h2>Authentifizierung</h2>
|
||||
<div id="auth-status" class="muted">Prüfe Anmeldestatus...</div>
|
||||
<div class="row">
|
||||
<a id="login-link" class="btn" href="https://cms.gallus-pub.ch/api/auth/gitea">Mit Gitea anmelden</a>
|
||||
<button id="btn-relogin">Neu anmelden</button>
|
||||
<button id="btn-logout">Abmelden</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="sec-events" style="display:none">
|
||||
<h2>Events verwalten</h2>
|
||||
<div class="events-row">
|
||||
<div class="card">
|
||||
<h3>Neues Event</h3>
|
||||
<label>Titel<input id="ev-title" /></label>
|
||||
<label>Datum<input id="ev-date" type="date" placeholder="z. B. 2025-12-31" /></label>
|
||||
<label>Beschreibung<textarea id="ev-desc" rows="4"></textarea></label>
|
||||
<label>Bild-Datei<input id="ev-file" type="file" accept="image/*" /></label>
|
||||
<button id="btn-create-ev">Event anlegen</button>
|
||||
<div id="ev-create-msg" class="muted"></div>
|
||||
</div>
|
||||
<div class="card" style="min-width:380px;">
|
||||
<h3>Liste</h3>
|
||||
<div class="toolbar">
|
||||
<span class="pill" id="lbl-order">Anzeige: automatisch nach Datum (neueste zuerst)</span>
|
||||
<button id="btn-toggle-reorder">Reihenfolge bearbeiten</button>
|
||||
<button id="btn-save-order" style="display:none">Reihenfolge speichern</button>
|
||||
<span id="order-msg" class="muted"></span>
|
||||
</div>
|
||||
</section>
|
||||
<div id="events-list" class="grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="sec-events" style="display:none">
|
||||
<h2>Events verwalten</h2>
|
||||
<div class="events-row">
|
||||
<div class="card">
|
||||
<h3>Neues Event</h3>
|
||||
<label>Titel<input id="ev-title" /></label>
|
||||
<label>Datum<input id="ev-date" type="date" placeholder="z. B. 2025-12-31" /></label>
|
||||
<label>Beschreibung<textarea id="ev-desc" rows="4"></textarea></label>
|
||||
<label>Bild-Datei<input id="ev-file" type="file" accept="image/*" /></label>
|
||||
<button id="btn-create-ev">Event anlegen</button>
|
||||
<div id="ev-create-msg" class="muted"></div>
|
||||
</div>
|
||||
<div class="card" style="min-width:380px;">
|
||||
<h3>Liste</h3>
|
||||
<div class="toolbar">
|
||||
<span class="pill" id="lbl-order">Anzeige: automatisch nach Datum (neueste zuerst)</span>
|
||||
<button id="btn-toggle-reorder">Reihenfolge bearbeiten</button>
|
||||
<button id="btn-save-order" style="display:none">Reihenfolge speichern</button>
|
||||
<span id="order-msg" class="muted"></span>
|
||||
</div>
|
||||
<div id="events-list" class="grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="sec-gallery" style="display:none">
|
||||
<h2>Gallery verwalten</h2>
|
||||
<div class="events-row">
|
||||
<div class="card">
|
||||
<h3>Neues Gallery-Bild</h3>
|
||||
<label>Bild-Datei<input id="gal-file" type="file" accept="image/*" /></label>
|
||||
<label>Alt-Text<input id="gal-alt" placeholder="Bildbeschreibung" /></label>
|
||||
<button id="btn-create-gal">Bild hochladen</button>
|
||||
<div id="gal-create-msg" class="muted"></div>
|
||||
</div>
|
||||
<div class="card" style="min-width:380px;">
|
||||
<h3>Gallery-Liste</h3>
|
||||
<div id="gallery-list" class="grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="sec-gallery" style="display:none">
|
||||
<h2>Gallery verwalten</h2>
|
||||
<div class="events-row">
|
||||
<div class="card">
|
||||
<h3>Neues Gallery-Bild</h3>
|
||||
<label>Bild-Datei<input id="gal-file" type="file" accept="image/*" /></label>
|
||||
<label>Alt-Text<input id="gal-alt" placeholder="Bildbeschreibung" /></label>
|
||||
<button id="btn-create-gal">Bild hochladen</button>
|
||||
<div id="gal-create-msg" class="muted"></div>
|
||||
</div>
|
||||
<div class="card" style="min-width:380px;">
|
||||
<h3>Gallery-Liste</h3>
|
||||
<div id="gallery-list" class="grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="sec-publish" style="display:none">
|
||||
<h2>Veröffentlichen</h2>
|
||||
<label>Commit-Message<input id="pub-msg" placeholder="Änderungen beschreiben" value="Update events" /></label>
|
||||
<button id="btn-publish">Publish</button>
|
||||
<div id="pub-status" class="muted"></div>
|
||||
</section>
|
||||
|
||||
<section id="sec-publish" style="display:none">
|
||||
<h2>Veröffentlichen</h2>
|
||||
<label>Commit-Message<input id="pub-msg" placeholder="Änderungen beschreiben" value="Update events" /></label>
|
||||
<button id="btn-publish">Publish</button>
|
||||
<div id="pub-status" class="muted"></div>
|
||||
</section>
|
||||
<script>
|
||||
const API_BASE = 'https://cms.gallus-pub.ch';
|
||||
|
||||
<script>
|
||||
// Base-URL des Backends (separate Subdomain)
|
||||
const API_BASE = 'https://cms.gallus-pub.ch';
|
||||
const api = async (path, opts = {}) => {
|
||||
const res = await fetch(API_BASE + path, { credentials: 'include', ...opts });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
return ct.includes('application/json') ? res.json() : res.text();
|
||||
};
|
||||
|
||||
const api = async (path, opts = {}) => {
|
||||
const res = await fetch(API_BASE + path, { credentials: 'include', ...opts });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
return ct.includes('application/json') ? res.json() : res.text();
|
||||
};
|
||||
// Helper to get full image URL
|
||||
function getImageUrl(imageUrl) {
|
||||
if (!imageUrl) return '';
|
||||
// API image endpoints need the base URL prefix
|
||||
if (imageUrl.startsWith('/api/')) {
|
||||
return API_BASE + imageUrl;
|
||||
}
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
async function refreshAuth() {
|
||||
try {
|
||||
const me = await api('/api/auth/me');
|
||||
document.getElementById('auth-status').textContent = `Angemeldet als ${me.user?.giteaUsername || 'Admin'}`;
|
||||
// UI-Bereiche für eingeloggte Nutzer einblenden
|
||||
document.getElementById('sec-events').style.display = '';
|
||||
document.getElementById('sec-gallery').style.display = '';
|
||||
document.getElementById('sec-publish').style.display = '';
|
||||
// Direkt Events laden und auf Sektion fokussieren
|
||||
await loadEvents();
|
||||
await loadGallery();
|
||||
document.getElementById('sec-events').scrollIntoView({ behavior: 'smooth' });
|
||||
} catch (e) {
|
||||
const el = document.getElementById('auth-status');
|
||||
el.textContent = 'Nicht angemeldet';
|
||||
// Kein Auto-Redirect, damit keine Schleife entsteht. Login-Button verwenden.
|
||||
document.getElementById('sec-events').style.display = 'none';
|
||||
document.getElementById('sec-gallery').style.display = 'none';
|
||||
document.getElementById('sec-publish').style.display = 'none';
|
||||
}
|
||||
async function refreshAuth() {
|
||||
try {
|
||||
const me = await api('/api/auth/me');
|
||||
document.getElementById('auth-status').textContent = `Angemeldet als ${me.user?.giteaUsername || 'Admin'}`;
|
||||
document.getElementById('sec-events').style.display = '';
|
||||
document.getElementById('sec-gallery').style.display = '';
|
||||
document.getElementById('sec-publish').style.display = '';
|
||||
await loadEvents();
|
||||
await loadGallery();
|
||||
document.getElementById('sec-events').scrollIntoView({ behavior: 'smooth' });
|
||||
} catch (e) {
|
||||
const el = document.getElementById('auth-status');
|
||||
el.textContent = 'Nicht angemeldet';
|
||||
document.getElementById('sec-events').style.display = 'none';
|
||||
document.getElementById('sec-gallery').style.display = 'none';
|
||||
document.getElementById('sec-publish').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const loginLink = document.getElementById('login-link');
|
||||
loginLink.addEventListener('click', (e) => {
|
||||
try {
|
||||
window.location.assign(API_BASE + '/api/auth/gitea');
|
||||
} catch {}
|
||||
});
|
||||
document.getElementById('btn-relogin').addEventListener('click', async () => {
|
||||
try { await api('/api/auth/logout', { method: 'POST' }); } catch {}
|
||||
document.cookie = 'token=; Path=/; Max-Age=0;';
|
||||
window.location.assign(API_BASE + '/api/auth/gitea');
|
||||
});
|
||||
document.getElementById('btn-logout').addEventListener('click', async () => {
|
||||
try { await api('/api/auth/logout', { method: 'POST' }); } catch {}
|
||||
document.cookie = 'token=; Path=/; Max-Age=0;';
|
||||
await refreshAuth();
|
||||
});
|
||||
|
||||
// Upload image and get base64 data back
|
||||
async function uploadEventImage(file) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const res = await fetch(API_BASE + '/api/events/upload', { method: 'POST', body: fd, credentials: 'include' });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json(); // Returns { imageData, mimeType }
|
||||
}
|
||||
|
||||
async function uploadGalleryImage(file, altText) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
if (altText) fd.append('altText', altText);
|
||||
fd.append('displayOrder', '0');
|
||||
const res = await fetch(API_BASE + '/api/gallery/upload', { method: 'POST', body: fd, credentials: 'include' });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
let reorderMode = false;
|
||||
let lastEvents = [];
|
||||
|
||||
function parseDateSafe(s){
|
||||
const d = new Date(s);
|
||||
return isNaN(+d) ? new Date(0) : d;
|
||||
}
|
||||
|
||||
async function loadEvents() {
|
||||
const listEl = document.getElementById('events-list');
|
||||
listEl.innerHTML = '<div class="muted">Lade...</div>';
|
||||
try {
|
||||
const data = await api('/api/events');
|
||||
listEl.innerHTML = '';
|
||||
lastEvents = (data.events || []).slice();
|
||||
let renderList = lastEvents.slice();
|
||||
if (!reorderMode) {
|
||||
renderList.sort((a,b) => +parseDateSafe(b.date) - +parseDateSafe(a.date));
|
||||
document.getElementById('lbl-order').textContent = 'Anzeige: automatisch nach Datum (neueste zuerst)';
|
||||
} else {
|
||||
renderList.sort((a,b) => (a.displayOrder??0) - (b.displayOrder??0));
|
||||
document.getElementById('lbl-order').textContent = 'Anzeige: manuelle Reihenfolge (drag & drop)';
|
||||
}
|
||||
|
||||
// Fallback: falls der Link von Browser/Extensions blockiert wäre
|
||||
const loginLink = document.getElementById('login-link');
|
||||
loginLink.addEventListener('click', (e) => {
|
||||
try {
|
||||
// Stelle sicher, dass Navigieren erzwungen wird
|
||||
window.location.assign(API_BASE + '/api/auth/gitea');
|
||||
} catch {}
|
||||
});
|
||||
document.getElementById('btn-relogin').addEventListener('click', async () => {
|
||||
try { await api('/api/auth/logout', { method: 'POST' }); } catch {}
|
||||
document.cookie = 'token=; Path=/; Max-Age=0;';
|
||||
window.location.assign(API_BASE + '/api/auth/gitea');
|
||||
});
|
||||
document.getElementById('btn-logout').addEventListener('click', async () => {
|
||||
try { await api('/api/auth/logout', { method: 'POST' }); } catch {}
|
||||
document.cookie = 'token=; Path=/; Max-Age=0;';
|
||||
await refreshAuth();
|
||||
});
|
||||
|
||||
// ========== Events & Publish ==========
|
||||
async function uploadEventImage(file) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const res = await fetch(API_BASE + '/api/events/upload', { method: 'POST', body: fd, credentials: 'include' });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function uploadGalleryImage(file, altText) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
if (altText) fd.append('altText', altText);
|
||||
fd.append('displayOrder', '0');
|
||||
const res = await fetch(API_BASE + '/api/gallery/upload', { method: 'POST', body: fd, credentials: 'include' });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
let reorderMode = false;
|
||||
let lastEvents = [];
|
||||
|
||||
function parseDateSafe(s){
|
||||
const d = new Date(s);
|
||||
return isNaN(+d) ? new Date(0) : d;
|
||||
}
|
||||
|
||||
async function loadEvents() {
|
||||
const listEl = document.getElementById('events-list');
|
||||
listEl.innerHTML = '<div class="muted">Lade...</div>';
|
||||
try {
|
||||
const data = await api('/api/events');
|
||||
listEl.innerHTML = '';
|
||||
// Merken, globale Liste aktualisieren
|
||||
lastEvents = (data.events || []).slice();
|
||||
let renderList = lastEvents.slice();
|
||||
if (!reorderMode) {
|
||||
// Automatisch nach Datum sortieren (neueste zuerst)
|
||||
renderList.sort((a,b) => +parseDateSafe(b.date) - +parseDateSafe(a.date));
|
||||
document.getElementById('lbl-order').textContent = 'Anzeige: automatisch nach Datum (neueste zuerst)';
|
||||
} else {
|
||||
// Nach displayOrder aufsteigend
|
||||
renderList.sort((a,b) => (a.displayOrder??0) - (b.displayOrder??0));
|
||||
document.getElementById('lbl-order').textContent = 'Anzeige: manuelle Reihenfolge (drag & drop)';
|
||||
}
|
||||
|
||||
renderList.forEach((ev, idx) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.setAttribute('draggable', String(reorderMode));
|
||||
card.dataset.id = ev.id;
|
||||
card.dataset.displayOrder = String(ev.displayOrder ?? idx);
|
||||
card.innerHTML = `
|
||||
renderList.forEach((ev, idx) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.setAttribute('draggable', String(reorderMode));
|
||||
card.dataset.id = ev.id;
|
||||
card.dataset.displayOrder = String(ev.displayOrder ?? idx);
|
||||
const imgUrl = getImageUrl(ev.imageUrl);
|
||||
card.innerHTML = `
|
||||
<div class="row" style="justify-content:space-between;align-items:center">
|
||||
<div><strong>${ev.title}</strong></div>
|
||||
${reorderMode ? '<span class="drag-handle">⇅ Ziehen</span>' : ''}
|
||||
${reorderMode ? '<span class="drag-handle">↕ Ziehen</span>' : ''}
|
||||
</div>
|
||||
<div class="muted">${ev.date}</div>
|
||||
<div>${ev.description || ''}</div>
|
||||
<div class="muted">Bild: ${ev.imageUrl || ''}</div>
|
||||
${imgUrl ? `<img src="${imgUrl}" alt="${ev.title}" class="thumb" style="margin-top:0.5rem;max-height:120px;object-fit:cover;" />` : '<div class="muted">Kein Bild</div>'}
|
||||
<div class="row-buttons">
|
||||
<button data-id="${ev.id}" class="btn-del-ev">Löschen</button>
|
||||
</div>`;
|
||||
listEl.appendChild(card);
|
||||
});
|
||||
listEl.querySelectorAll('.btn-del-ev').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.getAttribute('data-id');
|
||||
if (!id) return;
|
||||
if (!confirm('Event wirklich löschen?')) return;
|
||||
try { await api(`/api/events/${id}`, { method: 'DELETE' }); await loadEvents(); } catch(e){ alert('Fehler: '+e.message); }
|
||||
})
|
||||
})
|
||||
listEl.appendChild(card);
|
||||
});
|
||||
listEl.querySelectorAll('.btn-del-ev').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.getAttribute('data-id');
|
||||
if (!id) return;
|
||||
if (!confirm('Event wirklich löschen?')) return;
|
||||
try { await api(`/api/events/${id}`, { method: 'DELETE' }); await loadEvents(); } catch(e){ alert('Fehler: '+e.message); }
|
||||
})
|
||||
})
|
||||
|
||||
if (reorderMode) {
|
||||
enableDragAndDrop(listEl);
|
||||
}
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '<div class="muted">Fehler beim Laden</div>';
|
||||
console.error(e);
|
||||
}
|
||||
if (reorderMode) {
|
||||
enableDragAndDrop(listEl);
|
||||
}
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '<div class="muted">Fehler beim Laden</div>';
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Drag & Drop Reorder
|
||||
function enableDragAndDrop(container){
|
||||
let draggingEl = null;
|
||||
container.querySelectorAll('.card').forEach(card => {
|
||||
card.addEventListener('dragstart', (e) => {
|
||||
draggingEl = card; card.classList.add('dragging');
|
||||
e.dataTransfer.setData('text/plain', card.dataset.id || '');
|
||||
});
|
||||
card.addEventListener('dragend', () => { draggingEl = null; card.classList.remove('dragging'); });
|
||||
card.addEventListener('dragover', (e) => { e.preventDefault(); });
|
||||
card.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
const target = card;
|
||||
if (!draggingEl || draggingEl === target) return;
|
||||
const cards = Array.from(container.querySelectorAll('.card'));
|
||||
const draggingIdx = cards.indexOf(draggingEl);
|
||||
const targetIdx = cards.indexOf(target);
|
||||
if (draggingIdx < targetIdx) {
|
||||
target.after(draggingEl);
|
||||
} else {
|
||||
target.before(draggingEl);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btn-create-ev').addEventListener('click', async () => {
|
||||
const title = (document.getElementById('ev-title')).value.trim();
|
||||
const date = (document.getElementById('ev-date')).value.trim();
|
||||
const desc = (document.getElementById('ev-desc')).value.trim();
|
||||
const file = /** @type {HTMLInputElement} */ (document.getElementById('ev-file')).files[0];
|
||||
const msg = document.getElementById('ev-create-msg');
|
||||
msg.textContent = 'Lade Bild hoch...';
|
||||
try {
|
||||
let imageUrl = '';
|
||||
if (file) {
|
||||
const up = await uploadEventImage(file);
|
||||
imageUrl = up?.imageUrl || '';
|
||||
}
|
||||
msg.textContent = 'Lege Event an...';
|
||||
await api('/api/events', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, date, description: desc, imageUrl, displayOrder: 0, isPublished: true })
|
||||
});
|
||||
msg.textContent = 'Event erstellt';
|
||||
(document.getElementById('ev-title')).value = '';
|
||||
(document.getElementById('ev-date')).value = '';
|
||||
(document.getElementById('ev-desc')).value = '';
|
||||
(document.getElementById('ev-file')).value = '';
|
||||
await loadEvents();
|
||||
} catch(e){ msg.textContent = 'Fehler: '+e.message }
|
||||
function enableDragAndDrop(container){
|
||||
let draggingEl = null;
|
||||
container.querySelectorAll('.card').forEach(card => {
|
||||
card.addEventListener('dragstart', (e) => {
|
||||
draggingEl = card; card.classList.add('dragging');
|
||||
e.dataTransfer.setData('text/plain', card.dataset.id || '');
|
||||
});
|
||||
|
||||
document.getElementById('btn-publish').addEventListener('click', async () => {
|
||||
const s = document.getElementById('pub-status');
|
||||
const m = (document.getElementById('pub-msg')).value.trim() || 'Update events';
|
||||
s.textContent = 'Veröffentliche...';
|
||||
try {
|
||||
const res = await api('/api/publish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ commitMessage: m }) });
|
||||
s.textContent = res?.message || 'Veröffentlicht';
|
||||
} catch(e){ s.textContent = 'Fehler: '+e.message }
|
||||
});
|
||||
|
||||
// Toggle Reorder
|
||||
document.getElementById('btn-toggle-reorder').addEventListener('click', async () => {
|
||||
reorderMode = !reorderMode;
|
||||
document.getElementById('btn-save-order').style.display = reorderMode ? '' : 'none';
|
||||
await loadEvents();
|
||||
});
|
||||
|
||||
// Save Order
|
||||
document.getElementById('btn-save-order').addEventListener('click', async () => {
|
||||
const container = document.getElementById('events-list');
|
||||
card.addEventListener('dragend', () => { draggingEl = null; card.classList.remove('dragging'); });
|
||||
card.addEventListener('dragover', (e) => { e.preventDefault(); });
|
||||
card.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
const target = card;
|
||||
if (!draggingEl || draggingEl === target) return;
|
||||
const cards = Array.from(container.querySelectorAll('.card'));
|
||||
const orders = cards.map((c, idx) => ({ id: c.dataset.id, displayOrder: idx }));
|
||||
const msg = document.getElementById('order-msg');
|
||||
msg.textContent = 'Speichere Reihenfolge...';
|
||||
try {
|
||||
await api('/api/events/reorder', { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ orders }) });
|
||||
msg.textContent = 'Reihenfolge gespeichert';
|
||||
// Bleibe in Reorder-Mode oder verlasse ihn? Hier: verlassen
|
||||
reorderMode = false;
|
||||
document.getElementById('btn-save-order').style.display = 'none';
|
||||
await loadEvents();
|
||||
} catch(e){ msg.textContent = 'Fehler: '+e.message }
|
||||
const draggingIdx = cards.indexOf(draggingEl);
|
||||
const targetIdx = cards.indexOf(target);
|
||||
if (draggingIdx < targetIdx) {
|
||||
target.after(draggingEl);
|
||||
} else {
|
||||
target.before(draggingEl);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btn-create-ev').addEventListener('click', async () => {
|
||||
const title = (document.getElementById('ev-title')).value.trim();
|
||||
const date = (document.getElementById('ev-date')).value.trim();
|
||||
const desc = (document.getElementById('ev-desc')).value.trim();
|
||||
const file = (document.getElementById('ev-file')).files[0];
|
||||
const msg = document.getElementById('ev-create-msg');
|
||||
|
||||
if (!title || !date) {
|
||||
msg.textContent = 'Titel und Datum sind erforderlich';
|
||||
return;
|
||||
}
|
||||
|
||||
msg.textContent = 'Lade Bild hoch...';
|
||||
try {
|
||||
let imageData = '';
|
||||
let mimeType = '';
|
||||
|
||||
if (file) {
|
||||
const uploadResult = await uploadEventImage(file);
|
||||
imageData = uploadResult.imageData || '';
|
||||
mimeType = uploadResult.mimeType || '';
|
||||
}
|
||||
|
||||
msg.textContent = 'Lege Event an...';
|
||||
await api('/api/events', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
date,
|
||||
description: desc,
|
||||
imageData,
|
||||
mimeType,
|
||||
displayOrder: 0,
|
||||
isPublished: true
|
||||
})
|
||||
});
|
||||
|
||||
// ========== Gallery ==========
|
||||
async function loadGallery() {
|
||||
const listEl = document.getElementById('gallery-list');
|
||||
listEl.innerHTML = '<div class="muted">Lade...</div>';
|
||||
try {
|
||||
const data = await api('/api/gallery');
|
||||
listEl.innerHTML = '';
|
||||
const galleryImages = (data.images || []).slice();
|
||||
galleryImages.sort((a,b) => (a.displayOrder??0) - (b.displayOrder??0));
|
||||
msg.textContent = 'Event erstellt';
|
||||
document.getElementById('ev-title').value = '';
|
||||
document.getElementById('ev-date').value = '';
|
||||
document.getElementById('ev-desc').value = '';
|
||||
document.getElementById('ev-file').value = '';
|
||||
await loadEvents();
|
||||
} catch(e) {
|
||||
msg.textContent = 'Fehler: ' + e.message;
|
||||
}
|
||||
});
|
||||
|
||||
galleryImages.forEach((img) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.innerHTML = `
|
||||
<img src="${API_BASE}${img.imageUrl}" alt="${img.altText}" class="thumb" />
|
||||
document.getElementById('btn-publish').addEventListener('click', async () => {
|
||||
const s = document.getElementById('pub-status');
|
||||
const m = (document.getElementById('pub-msg')).value.trim() || 'Update events';
|
||||
s.textContent = 'Veröffentliche...';
|
||||
try {
|
||||
const res = await api('/api/publish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ commitMessage: m }) });
|
||||
s.textContent = res?.message || 'Veröffentlicht';
|
||||
} catch(e){ s.textContent = 'Fehler: '+e.message }
|
||||
});
|
||||
|
||||
document.getElementById('btn-toggle-reorder').addEventListener('click', async () => {
|
||||
reorderMode = !reorderMode;
|
||||
document.getElementById('btn-save-order').style.display = reorderMode ? '' : 'none';
|
||||
await loadEvents();
|
||||
});
|
||||
|
||||
document.getElementById('btn-save-order').addEventListener('click', async () => {
|
||||
const container = document.getElementById('events-list');
|
||||
const cards = Array.from(container.querySelectorAll('.card'));
|
||||
const orders = cards.map((c, idx) => ({ id: c.dataset.id, displayOrder: idx }));
|
||||
const msg = document.getElementById('order-msg');
|
||||
msg.textContent = 'Speichere Reihenfolge...';
|
||||
try {
|
||||
await api('/api/events/reorder', { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ orders }) });
|
||||
msg.textContent = 'Reihenfolge gespeichert';
|
||||
reorderMode = false;
|
||||
document.getElementById('btn-save-order').style.display = 'none';
|
||||
await loadEvents();
|
||||
} catch(e){ msg.textContent = 'Fehler: '+e.message }
|
||||
});
|
||||
|
||||
// Gallery
|
||||
async function loadGallery() {
|
||||
const listEl = document.getElementById('gallery-list');
|
||||
listEl.innerHTML = '<div class="muted">Lade...</div>';
|
||||
try {
|
||||
const data = await api('/api/gallery');
|
||||
listEl.innerHTML = '';
|
||||
const galleryImages = (data.images || []).slice();
|
||||
galleryImages.sort((a,b) => (a.displayOrder??0) - (b.displayOrder??0));
|
||||
|
||||
galleryImages.forEach((img) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
const imgUrl = getImageUrl(img.imageUrl);
|
||||
card.innerHTML = `
|
||||
<img src="${imgUrl}" alt="${img.altText}" class="thumb" />
|
||||
<div class="muted">${img.altText || ''}</div>
|
||||
<div class="row-buttons">
|
||||
<button data-id="${img.id}" class="btn-del-gal">Löschen</button>
|
||||
</div>`;
|
||||
listEl.appendChild(card);
|
||||
});
|
||||
listEl.querySelectorAll('.btn-del-gal').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.getAttribute('data-id');
|
||||
if (!id) return;
|
||||
if (!confirm('Bild wirklich löschen?')) return;
|
||||
try { await api(`/api/gallery/${id}`, { method: 'DELETE' }); await loadGallery(); } catch(e){ alert('Fehler: '+e.message); }
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '<div class="muted">Fehler beim Laden</div>';
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('btn-create-gal').addEventListener('click', async () => {
|
||||
const file = /** @type {HTMLInputElement} */ (document.getElementById('gal-file')).files[0];
|
||||
const alt = (document.getElementById('gal-alt')).value.trim();
|
||||
const msg = document.getElementById('gal-create-msg');
|
||||
if (!file) {
|
||||
msg.textContent = 'Bitte Datei auswählen';
|
||||
return;
|
||||
}
|
||||
msg.textContent = 'Lade Bild hoch...';
|
||||
try {
|
||||
await uploadGalleryImage(file, alt);
|
||||
msg.textContent = 'Bild hochgeladen';
|
||||
(document.getElementById('gal-file')).value = '';
|
||||
(document.getElementById('gal-alt')).value = '';
|
||||
await loadGallery();
|
||||
} catch(e){ msg.textContent = 'Fehler: '+e.message }
|
||||
listEl.appendChild(card);
|
||||
});
|
||||
listEl.querySelectorAll('.btn-del-gal').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.getAttribute('data-id');
|
||||
if (!id) return;
|
||||
if (!confirm('Bild wirklich löschen?')) return;
|
||||
try { await api(`/api/gallery/${id}`, { method: 'DELETE' }); await loadGallery(); } catch(e){ alert('Fehler: '+e.message); }
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '<div class="muted">Fehler beim Laden</div>';
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
refreshAuth();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
document.getElementById('btn-create-gal').addEventListener('click', async () => {
|
||||
const file = (document.getElementById('gal-file')).files[0];
|
||||
const alt = (document.getElementById('gal-alt')).value.trim();
|
||||
const msg = document.getElementById('gal-create-msg');
|
||||
if (!file) {
|
||||
msg.textContent = 'Bitte Datei auswählen';
|
||||
return;
|
||||
}
|
||||
msg.textContent = 'Lade Bild hoch...';
|
||||
try {
|
||||
await uploadGalleryImage(file, alt);
|
||||
msg.textContent = 'Bild hochgeladen';
|
||||
document.getElementById('gal-file').value = '';
|
||||
document.getElementById('gal-alt').value = '';
|
||||
await loadGallery();
|
||||
} catch(e){ msg.textContent = 'Fehler: '+e.message }
|
||||
});
|
||||
|
||||
refreshAuth();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user