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'),
|
commitMessage: text('commit_message'),
|
||||||
publishedAt: integer('published_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
|
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 contentRoute from './routes/content.js';
|
||||||
import settingsRoute from './routes/settings.js';
|
import settingsRoute from './routes/settings.js';
|
||||||
import publishRoute from './routes/publish.js';
|
import publishRoute from './routes/publish.js';
|
||||||
|
import bannersRoute from './routes/banners.js';
|
||||||
|
|
||||||
// Validate environment variables
|
// Validate environment variables
|
||||||
try {
|
try {
|
||||||
@ -78,6 +79,7 @@ fastify.register(galleryRoute, { prefix: '/api' });
|
|||||||
fastify.register(contentRoute, { prefix: '/api' });
|
fastify.register(contentRoute, { prefix: '/api' });
|
||||||
fastify.register(settingsRoute, { prefix: '/api' });
|
fastify.register(settingsRoute, { prefix: '/api' });
|
||||||
fastify.register(publishRoute, { prefix: '/api' });
|
fastify.register(publishRoute, { prefix: '/api' });
|
||||||
|
fastify.register(bannersRoute, { prefix: '/api' });
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
fastify.get('/health', async () => {
|
fastify.get('/health', async () => {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const oldEvents = [
|
|||||||
{
|
{
|
||||||
image: "/images/events/event_karaoke.jpg",
|
image: "/images/events/event_karaoke.jpg",
|
||||||
title: "Karaoke",
|
title: "Karaoke",
|
||||||
date: "2025-12-31",
|
date: "2025-12-31", // Set as ongoing event
|
||||||
description: `Bei uns gibt es Karaoke Mi-Sa!! <br>
|
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>
|
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>`,
|
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",
|
image: "/images/events/event_pub-quiz.jpg",
|
||||||
title: "Pub Quiz",
|
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>
|
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>
|
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>
|
Auch Einzelpersonen sind herzlich willkommen! <br>
|
||||||
@ -75,43 +75,62 @@ const oldGalleryImages = [
|
|||||||
{ src: "/images/gallery/Gallery5.png", alt: "Gallery 5" },
|
{ 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 projectRoot = path.join(process.cwd(), '..');
|
||||||
const fullSourcePath = path.join(projectRoot, 'public', sourcePath);
|
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)) {
|
if (!fs.existsSync(fullSourcePath)) {
|
||||||
console.error(`Source file not found: ${fullSourcePath}`);
|
console.error(`Source file not found: ${fullSourcePath}`);
|
||||||
throw new Error(`Source file not found: ${fullSourcePath}`);
|
throw new Error(`Source file not found: ${fullSourcePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to webp and get buffer
|
// Convert to webp and copy
|
||||||
const buffer = await sharp(fullSourcePath)
|
await sharp(fullSourcePath)
|
||||||
.rotate()
|
.rotate() // Auto-rotate based on EXIF
|
||||||
.resize({ width: 1600, withoutEnlargement: true })
|
.resize({ width: 1600, withoutEnlargement: true })
|
||||||
.webp({ quality: 85 })
|
.webp({ quality: 85 })
|
||||||
.toBuffer();
|
.toFile(destPath);
|
||||||
|
|
||||||
return {
|
return `/images/${path.relative(destDir, destPath).replace(/\\/g, '/')}`;
|
||||||
imageData: buffer.toString('base64'),
|
|
||||||
mimeType: 'image/webp',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migrateEvents() {
|
async function migrateEvents() {
|
||||||
console.log('\n=== Migrating Events ===\n');
|
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) {
|
for (const event of oldEvents) {
|
||||||
try {
|
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({
|
const [newEvent] = await db.insert(events).values({
|
||||||
title: event.title,
|
title: event.title,
|
||||||
date: event.date,
|
date: event.date,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
imageData,
|
imageUrl: newImageUrl,
|
||||||
mimeType,
|
|
||||||
displayOrder: event.displayOrder,
|
displayOrder: event.displayOrder,
|
||||||
isPublished: true,
|
isPublished: true,
|
||||||
}).returning();
|
}).returning();
|
||||||
@ -126,14 +145,21 @@ async function migrateEvents() {
|
|||||||
async function migrateGallery() {
|
async function migrateGallery() {
|
||||||
console.log('\n=== Migrating Gallery Images ===\n');
|
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++) {
|
for (let i = 0; i < oldGalleryImages.length; i++) {
|
||||||
const img = oldGalleryImages[i];
|
const img = oldGalleryImages[i];
|
||||||
try {
|
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({
|
const [newImage] = await db.insert(galleryImages).values({
|
||||||
imageData,
|
imageUrl: newImageUrl,
|
||||||
mimeType,
|
|
||||||
altText: img.alt,
|
altText: img.alt,
|
||||||
displayOrder: i,
|
displayOrder: i,
|
||||||
isPublished: true,
|
isPublished: true,
|
||||||
@ -149,6 +175,7 @@ async function migrateGallery() {
|
|||||||
async function main() {
|
async function main() {
|
||||||
console.log('Starting migration of old data...\n');
|
console.log('Starting migration of old data...\n');
|
||||||
console.log('Working directory:', process.cwd());
|
console.log('Working directory:', process.cwd());
|
||||||
|
console.log('Data directory:', process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await migrateEvents();
|
await migrateEvents();
|
||||||
@ -160,4 +187,4 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@ -84,6 +84,24 @@ const title = 'Admin';
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section id="sec-publish" style="display:none">
|
||||||
<h2>Veröffentlichen</h2>
|
<h2>Veröffentlichen</h2>
|
||||||
<label>Commit-Message<input id="pub-msg" placeholder="Änderungen beschreiben" value="Update events" /></label>
|
<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
|
// UI-Bereiche für eingeloggte Nutzer einblenden
|
||||||
document.getElementById('sec-events').style.display = '';
|
document.getElementById('sec-events').style.display = '';
|
||||||
document.getElementById('sec-gallery').style.display = '';
|
document.getElementById('sec-gallery').style.display = '';
|
||||||
|
document.getElementById('sec-banner').style.display = '';
|
||||||
document.getElementById('sec-publish').style.display = '';
|
document.getElementById('sec-publish').style.display = '';
|
||||||
// Direkt Events laden und auf Sektion fokussieren
|
// Direkt Events laden und auf Sektion fokussieren
|
||||||
await loadEvents();
|
await loadEvents();
|
||||||
await loadGallery();
|
await loadGallery();
|
||||||
|
await loadBanners();
|
||||||
document.getElementById('sec-events').scrollIntoView({ behavior: 'smooth' });
|
document.getElementById('sec-events').scrollIntoView({ behavior: 'smooth' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const el = document.getElementById('auth-status');
|
const el = document.getElementById('auth-status');
|
||||||
@ -120,6 +140,7 @@ const title = 'Admin';
|
|||||||
// Kein Auto-Redirect, damit keine Schleife entsteht. Login-Button verwenden.
|
// Kein Auto-Redirect, damit keine Schleife entsteht. Login-Button verwenden.
|
||||||
document.getElementById('sec-events').style.display = 'none';
|
document.getElementById('sec-events').style.display = 'none';
|
||||||
document.getElementById('sec-gallery').style.display = 'none';
|
document.getElementById('sec-gallery').style.display = 'none';
|
||||||
|
document.getElementById('sec-banner').style.display = 'none';
|
||||||
document.getElementById('sec-publish').style.display = 'none';
|
document.getElementById('sec-publish').style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -202,7 +223,7 @@ const title = 'Admin';
|
|||||||
</div>
|
</div>
|
||||||
<div class="muted">${ev.date}</div>
|
<div class="muted">${ev.date}</div>
|
||||||
<div>${ev.description || ''}</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">
|
<div class="row-buttons">
|
||||||
<button data-id="${ev.id}" class="btn-del-ev">Löschen</button>
|
<button data-id="${ev.id}" class="btn-del-ev">Löschen</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -366,6 +387,94 @@ const title = 'Admin';
|
|||||||
} catch(e){ msg.textContent = 'Fehler: '+e.message }
|
} 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();
|
refreshAuth();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import Layout from "../components/Layout.astro";
|
import Layout from "../components/Layout.astro";
|
||||||
import Hero from "../components/Hero.astro";
|
import Hero from "../components/Hero.astro";
|
||||||
|
import Banner from "../components/Banner.astro";
|
||||||
import Welcome from "../components/Welcome.astro";
|
import Welcome from "../components/Welcome.astro";
|
||||||
import EventsGrid from "../components/EventsGrid.astro";
|
import EventsGrid from "../components/EventsGrid.astro";
|
||||||
import Drinks from "../components/Drinks.astro";
|
import Drinks from "../components/Drinks.astro";
|
||||||
@ -76,6 +77,7 @@ const images = [
|
|||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Hero id="hero" />
|
<Hero id="hero" />
|
||||||
|
<Banner />
|
||||||
<Welcome id="welcome" />
|
<Welcome id="welcome" />
|
||||||
<EventsGrid id="events" events={events} />
|
<EventsGrid id="events" events={events} />
|
||||||
<ImageCarousel id="gallery" images={images} />
|
<ImageCarousel id="gallery" images={images} />
|
||||||
|
|||||||
Reference in New Issue
Block a user