Compare commits
2 Commits
d0101b2974
...
20feee84a6
| Author | SHA1 | Date | |
|---|---|---|---|
| 20feee84a6 | |||
| bf7e38ba2d |
@ -60,3 +60,14 @@ export const publishHistory = sqliteTable('publish_history', {
|
||||
commitMessage: text('commit_message'),
|
||||
publishedAt: integer('published_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
|
||||
});
|
||||
|
||||
// Banner table (for announcements like holidays, special info)
|
||||
export const banners = sqliteTable('banners', {
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
text: text('text').notNull(),
|
||||
startDate: text('start_date').notNull(), // ISO date string
|
||||
endDate: text('end_date').notNull(), // ISO date string
|
||||
isActive: integer('is_active', { mode: 'boolean' }).default(true),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
|
||||
});
|
||||
|
||||
@ -16,6 +16,7 @@ import galleryRoute from './routes/gallery.js';
|
||||
import contentRoute from './routes/content.js';
|
||||
import settingsRoute from './routes/settings.js';
|
||||
import publishRoute from './routes/publish.js';
|
||||
import bannersRoute from './routes/banners.js';
|
||||
|
||||
// Validate environment variables
|
||||
try {
|
||||
@ -78,6 +79,7 @@ fastify.register(galleryRoute, { prefix: '/api' });
|
||||
fastify.register(contentRoute, { prefix: '/api' });
|
||||
fastify.register(settingsRoute, { prefix: '/api' });
|
||||
fastify.register(publishRoute, { prefix: '/api' });
|
||||
fastify.register(bannersRoute, { prefix: '/api' });
|
||||
|
||||
// Health check
|
||||
fastify.get('/health', async () => {
|
||||
|
||||
@ -9,7 +9,7 @@ const oldEvents = [
|
||||
{
|
||||
image: "/images/events/event_karaoke.jpg",
|
||||
title: "Karaoke",
|
||||
date: "2025-12-31",
|
||||
date: "2025-12-31", // Set as ongoing event
|
||||
description: `Bei uns gibt es Karaoke Mi-Sa!! <br>
|
||||
Seid ihr eine Gruppe und lieber unter euch? ..unseren 2.Stock kannst du auch mieten ;) <br>
|
||||
Reserviere am besten gleich per Whatsapp <a href="tel:+41772322770">077 232 27 70</a>`,
|
||||
@ -18,7 +18,7 @@ Reserviere am besten gleich per Whatsapp <a href="tel:+41772322770">077 232 27 7
|
||||
{
|
||||
image: "/images/events/event_pub-quiz.jpg",
|
||||
title: "Pub Quiz",
|
||||
date: "2025-12-31",
|
||||
date: "2025-12-31", // Set as ongoing event
|
||||
description: `Jeden Freitag findet unser <b>Pub Quiz</b> statt. Gespielt wird tischweise in 3-4 Runden. <br>
|
||||
Jede Woche gibt es ein anderes Thema. Es geht um Ruhm und Ehre und zusätzlich werden die Sieger der Herzen durch das Publikum gekürt! <3 <br>
|
||||
Auch Einzelpersonen sind herzlich willkommen! <br>
|
||||
@ -75,43 +75,62 @@ const oldGalleryImages = [
|
||||
{ src: "/images/gallery/Gallery5.png", alt: "Gallery 5" },
|
||||
];
|
||||
|
||||
async function processImageToBase64(sourcePath: string): Promise<{ imageData: string; mimeType: string }> {
|
||||
async function copyAndConvertImage(
|
||||
sourcePath: string,
|
||||
destDir: string,
|
||||
filename: string
|
||||
): Promise<string> {
|
||||
const projectRoot = path.join(process.cwd(), '..');
|
||||
const fullSourcePath = path.join(projectRoot, 'public', sourcePath);
|
||||
|
||||
console.log(`Processing: ${fullSourcePath}`);
|
||||
// Ensure destination directory exists
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
}
|
||||
|
||||
const ext = path.extname(filename);
|
||||
const baseName = path.basename(filename, ext);
|
||||
const webpFilename = `${baseName}.webp`;
|
||||
const destPath = path.join(destDir, webpFilename);
|
||||
|
||||
console.log(`Processing: ${fullSourcePath} -> ${destPath}`);
|
||||
|
||||
// Check if source exists
|
||||
if (!fs.existsSync(fullSourcePath)) {
|
||||
console.error(`Source file not found: ${fullSourcePath}`);
|
||||
throw new Error(`Source file not found: ${fullSourcePath}`);
|
||||
}
|
||||
|
||||
// Convert to webp and get buffer
|
||||
const buffer = await sharp(fullSourcePath)
|
||||
.rotate()
|
||||
// Convert to webp and copy
|
||||
await sharp(fullSourcePath)
|
||||
.rotate() // Auto-rotate based on EXIF
|
||||
.resize({ width: 1600, withoutEnlargement: true })
|
||||
.webp({ quality: 85 })
|
||||
.toBuffer();
|
||||
.toFile(destPath);
|
||||
|
||||
return {
|
||||
imageData: buffer.toString('base64'),
|
||||
mimeType: 'image/webp',
|
||||
};
|
||||
return `/images/${path.relative(destDir, destPath).replace(/\\/g, '/')}`;
|
||||
}
|
||||
|
||||
async function migrateEvents() {
|
||||
console.log('\n=== Migrating Events ===\n');
|
||||
|
||||
const dataDir = process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data');
|
||||
const eventsImageDir = path.join(dataDir, 'images', 'events');
|
||||
|
||||
for (const event of oldEvents) {
|
||||
try {
|
||||
const { imageData, mimeType } = await processImageToBase64(event.image);
|
||||
const filename = path.basename(event.image);
|
||||
const newImageUrl = await copyAndConvertImage(
|
||||
event.image,
|
||||
eventsImageDir,
|
||||
filename
|
||||
);
|
||||
|
||||
const [newEvent] = await db.insert(events).values({
|
||||
title: event.title,
|
||||
date: event.date,
|
||||
description: event.description,
|
||||
imageData,
|
||||
mimeType,
|
||||
imageUrl: newImageUrl,
|
||||
displayOrder: event.displayOrder,
|
||||
isPublished: true,
|
||||
}).returning();
|
||||
@ -126,14 +145,21 @@ async function migrateEvents() {
|
||||
async function migrateGallery() {
|
||||
console.log('\n=== Migrating Gallery Images ===\n');
|
||||
|
||||
const dataDir = process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data');
|
||||
const galleryImageDir = path.join(dataDir, 'images', 'gallery');
|
||||
|
||||
for (let i = 0; i < oldGalleryImages.length; i++) {
|
||||
const img = oldGalleryImages[i];
|
||||
try {
|
||||
const { imageData, mimeType } = await processImageToBase64(img.src);
|
||||
const filename = path.basename(img.src);
|
||||
const newImageUrl = await copyAndConvertImage(
|
||||
img.src,
|
||||
galleryImageDir,
|
||||
filename
|
||||
);
|
||||
|
||||
const [newImage] = await db.insert(galleryImages).values({
|
||||
imageData,
|
||||
mimeType,
|
||||
imageUrl: newImageUrl,
|
||||
altText: img.alt,
|
||||
displayOrder: i,
|
||||
isPublished: true,
|
||||
@ -149,6 +175,7 @@ async function migrateGallery() {
|
||||
async function main() {
|
||||
console.log('Starting migration of old data...\n');
|
||||
console.log('Working directory:', process.cwd());
|
||||
console.log('Data directory:', process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data'));
|
||||
|
||||
try {
|
||||
await migrateEvents();
|
||||
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
@ -202,7 +223,7 @@ const title = 'Admin';
|
||||
</div>
|
||||
<div class="muted">${ev.date}</div>
|
||||
<div>${ev.description || ''}</div>
|
||||
<div class="muted">Bild: ${ev.imageUrl || ''}</div>
|
||||
${ev.imageUrl ? `<img src="${API_BASE}${ev.imageUrl}" alt="${ev.title}" class="thumb" style="margin-top:0.5rem;" />` : '<div class="muted">Kein Bild</div>'}
|
||||
<div class="row-buttons">
|
||||
<button data-id="${ev.id}" class="btn-del-ev">Löschen</button>
|
||||
</div>`;
|
||||
@ -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>
|
||||
|
||||
@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user