feat: Add banner management feature and improve event/gallery image handling

- Introduced a new "Banners" feature, enabling banner creation, management, and display across the admin panel and frontend.
- Enhanced image handling for events and gallery by converting images to optimized webp format.
- Added `banners` table in the database schema for storing announcements.
- Integrated new `/api/banners` route in backend for banner operations.
- Updated `index.astro` to include banner display component.
- Added supporting UI and APIs in the admin panel for banner management.
This commit is contained in:
2025-12-17 20:47:38 +01:00
parent d0101b2974
commit bf7e38ba2d
5 changed files with 172 additions and 21 deletions

View File

@ -84,6 +84,24 @@ const title = 'Admin';
</div>
</section>
<section id="sec-banner" style="display:none">
<h2>Banner verwalten</h2>
<div class="events-row">
<div class="card">
<h3>Neuen Banner erstellen</h3>
<label>Text<textarea id="banner-text" rows="3" placeholder="z.B. Wir sind vom 24.12. bis 02.01. geschlossen"></textarea></label>
<label>Von (Datum)<input id="banner-start" type="date" /></label>
<label>Bis (Datum)<input id="banner-end" type="date" /></label>
<button id="btn-create-banner">Banner erstellen</button>
<div id="banner-create-msg" class="muted"></div>
</div>
<div class="card" style="min-width:380px;">
<h3>Banner-Liste</h3>
<div id="banner-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>
@ -109,10 +127,12 @@ const title = 'Admin';
// UI-Bereiche für eingeloggte Nutzer einblenden
document.getElementById('sec-events').style.display = '';
document.getElementById('sec-gallery').style.display = '';
document.getElementById('sec-banner').style.display = '';
document.getElementById('sec-publish').style.display = '';
// Direkt Events laden und auf Sektion fokussieren
await loadEvents();
await loadGallery();
await loadBanners();
document.getElementById('sec-events').scrollIntoView({ behavior: 'smooth' });
} catch (e) {
const el = document.getElementById('auth-status');
@ -120,6 +140,7 @@ const title = 'Admin';
// 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-banner').style.display = 'none';
document.getElementById('sec-publish').style.display = 'none';
}
}
@ -366,6 +387,94 @@ const title = 'Admin';
} catch(e){ msg.textContent = 'Fehler: '+e.message }
});
// ========== Banner ==========
async function loadBanners() {
const listEl = document.getElementById('banner-list');
listEl.innerHTML = '<div class="muted">Lade...</div>';
try {
const data = await api('/api/banners');
listEl.innerHTML = '';
const bannersList = (data.banners || []).slice();
bannersList.sort((a,b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
bannersList.forEach((banner) => {
const card = document.createElement('div');
card.className = 'card';
const statusText = banner.isActive ? '✓ Aktiv' : '✗ Inaktiv';
card.innerHTML = `
<div><strong>${banner.text.substring(0, 60)}${banner.text.length > 60 ? '...' : ''}</strong></div>
<div class="muted">Von: ${banner.startDate}</div>
<div class="muted">Bis: ${banner.endDate}</div>
<div class="pill">${statusText}</div>
<div class="row-buttons">
<button data-id="${banner.id}" class="btn-toggle-banner">${banner.isActive ? 'Deaktivieren' : 'Aktivieren'}</button>
<button data-id="${banner.id}" class="btn-del-banner">Löschen</button>
</div>`;
listEl.appendChild(card);
});
listEl.querySelectorAll('.btn-toggle-banner').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.getAttribute('data-id');
if (!id) return;
const banner = bannersList.find(b => b.id === id);
if (!banner) return;
try {
await api(`/api/banners/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: banner.text,
startDate: banner.startDate,
endDate: banner.endDate,
isActive: !banner.isActive
})
});
await loadBanners();
} catch(e){ alert('Fehler: '+e.message); }
})
});
listEl.querySelectorAll('.btn-del-banner').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.getAttribute('data-id');
if (!id) return;
if (!confirm('Banner wirklich löschen?')) return;
try { await api(`/api/banners/${id}`, { method: 'DELETE' }); await loadBanners(); } catch(e){ alert('Fehler: '+e.message); }
})
})
} catch (e) {
listEl.innerHTML = '<div class="muted">Fehler beim Laden</div>';
console.error(e);
}
}
document.getElementById('btn-create-banner').addEventListener('click', async () => {
const text = (document.getElementById('banner-text')).value.trim();
const startDate = (document.getElementById('banner-start')).value.trim();
const endDate = (document.getElementById('banner-end')).value.trim();
const msg = document.getElementById('banner-create-msg');
if (!text || !startDate || !endDate) {
msg.textContent = 'Bitte alle Felder ausfüllen';
return;
}
msg.textContent = 'Erstelle Banner...';
try {
await api('/api/banners', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, startDate, endDate, isActive: true })
});
msg.textContent = 'Banner erstellt';
(document.getElementById('banner-text')).value = '';
(document.getElementById('banner-start')).value = '';
(document.getElementById('banner-end')).value = '';
await loadBanners();
} catch(e){ msg.textContent = 'Fehler: '+e.message }
});
refreshAuth();
</script>
</body>

View File

@ -1,6 +1,7 @@
---
import Layout from "../components/Layout.astro";
import Hero from "../components/Hero.astro";
import Banner from "../components/Banner.astro";
import Welcome from "../components/Welcome.astro";
import EventsGrid from "../components/EventsGrid.astro";
import Drinks from "../components/Drinks.astro";
@ -76,6 +77,7 @@ const images = [
<Layout>
<Hero id="hero" />
<Banner />
<Welcome id="welcome" />
<EventsGrid id="events" events={events} />
<ImageCarousel id="gallery" images={images} />