Compare commits
12 Commits
feat/cms
...
af4877300f
| Author | SHA1 | Date | |
|---|---|---|---|
| af4877300f | |||
| 4a103cf7d6 | |||
| 97e7f88906 | |||
| 807c56de5a | |||
| 8ca30ae5f3 | |||
| 8f1254840c | |||
| 8b2d00385a | |||
| c55e274718 | |||
| e9a95ccf8d | |||
| b16ac76620 | |||
| 0e03b9dea9 | |||
| da3a950a1a |
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,3 +22,4 @@ pnpm-debug.log*
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
/ai/
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
|
||||
steps:
|
||||
deploy:
|
||||
deploy_frontend:
|
||||
image: node:20
|
||||
environment:
|
||||
FLY_API_TOKEN:
|
||||
@ -7,10 +13,15 @@ steps:
|
||||
commands:
|
||||
- curl -L https://fly.io/install.sh | sh
|
||||
- export PATH="$HOME/.fly/bin:$PATH"
|
||||
- flyctl deploy --config fly.toml --app gallus-pub
|
||||
- flyctl deploy --config fly.toml --app gallus-pub --remote-only
|
||||
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
deploy_backend:
|
||||
image: node:20
|
||||
environment:
|
||||
FLY_API_TOKEN:
|
||||
from_secret: FLY_API_TOKEN
|
||||
commands:
|
||||
- cd backend
|
||||
- curl -L https://fly.io/install.sh | sh
|
||||
- export PATH="$HOME/.fly/bin:$PATH"
|
||||
- flyctl deploy --config fly.toml --app gallus-cms-backend --remote-only
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Deployment Guide
|
||||
|
||||
## Prerequisites
|
||||
## Prerequisite
|
||||
|
||||
1. Fly.io CLI installed: `curl -L https://fly.io/install.sh | sh`
|
||||
2. Fly.io account: `flyctl auth login`
|
||||
|
||||
@ -3,6 +3,8 @@ app = "gallus-cms-backend"
|
||||
primary_region = "ams"
|
||||
|
||||
[build]
|
||||
# Ensure Fly uses the Dockerfile in this backend directory
|
||||
dockerfile = "Dockerfile"
|
||||
|
||||
[env]
|
||||
PORT = "8080"
|
||||
@ -10,6 +12,10 @@ primary_region = "ams"
|
||||
GITEA_URL = "https://git.bookageek.ch"
|
||||
DATABASE_PATH = "/app/data/gallus_cms.db"
|
||||
GIT_WORKSPACE_DIR = "/app/data/workspace"
|
||||
# Cross-site frontend and OAuth
|
||||
FRONTEND_URL = "https://gallus-pub.ch"
|
||||
CORS_ORIGIN = "https://gallus-pub.ch"
|
||||
GITEA_REDIRECT_URI = "https://cms.gallus-pub.ch/api/auth/callback"
|
||||
|
||||
[http_service]
|
||||
internal_port = 8080
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"@fastify/cookie": "^9.3.1",
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"@fastify/jwt": "^8.0.0",
|
||||
"@fastify/static": "^6.12.0",
|
||||
"@fastify/multipart": "^8.1.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
|
||||
@ -2,14 +2,103 @@ import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||||
import Database from 'better-sqlite3';
|
||||
import * as schema from '../db/schema.js';
|
||||
import { env } from './env.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
if (!env.DATABASE_PATH) {
|
||||
throw new Error('DATABASE_PATH environment variable is not set');
|
||||
}
|
||||
|
||||
// Ensure directory exists BEFORE opening the database file
|
||||
const dbDir = path.dirname(env.DATABASE_PATH);
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
const sqlite = new Database(env.DATABASE_PATH);
|
||||
|
||||
// Enable WAL mode for better concurrent access
|
||||
sqlite.pragma('journal_mode = WAL');
|
||||
|
||||
export const db = drizzle(sqlite, { schema });
|
||||
|
||||
// Auto-create tables if they don't exist
|
||||
export function initDatabase() {
|
||||
console.log('🔧 Initializing database...');
|
||||
|
||||
try {
|
||||
// Check if users table exists (acts as a sentinel for initial setup)
|
||||
const tableCheck = sqlite
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
|
||||
.get();
|
||||
|
||||
if (!tableCheck) {
|
||||
console.log('📝 Creating database schema...');
|
||||
|
||||
sqlite.exec(`
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
gitea_id TEXT UNIQUE NOT NULL,
|
||||
gitea_username TEXT NOT NULL,
|
||||
gitea_email TEXT,
|
||||
display_name TEXT,
|
||||
avatar_url TEXT,
|
||||
role TEXT DEFAULT 'admin',
|
||||
created_at INTEGER DEFAULT (unixepoch()),
|
||||
last_login INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
image_url TEXT NOT NULL,
|
||||
display_order INTEGER NOT NULL,
|
||||
is_published INTEGER DEFAULT 1,
|
||||
created_at INTEGER DEFAULT (unixepoch()),
|
||||
updated_at INTEGER DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gallery_images (
|
||||
id TEXT PRIMARY KEY,
|
||||
image_url TEXT NOT NULL,
|
||||
alt_text TEXT NOT NULL,
|
||||
display_order INTEGER NOT NULL,
|
||||
is_published INTEGER DEFAULT 1,
|
||||
created_at INTEGER DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS content_sections (
|
||||
id TEXT PRIMARY KEY,
|
||||
section_name TEXT UNIQUE NOT NULL,
|
||||
content_json TEXT NOT NULL,
|
||||
updated_at INTEGER DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at INTEGER DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS publish_history (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT REFERENCES users(id),
|
||||
commit_hash TEXT,
|
||||
commit_message TEXT,
|
||||
published_at INTEGER DEFAULT (unixepoch())
|
||||
);
|
||||
`);
|
||||
|
||||
console.log('✅ Database schema created successfully!');
|
||||
} else {
|
||||
console.log('✅ Database already initialized.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error initializing database:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,9 @@ import multipart from '@fastify/multipart';
|
||||
import cookie from '@fastify/cookie';
|
||||
import { authenticate } from './middleware/auth.middleware.js';
|
||||
import { env, validateEnv } from './config/env.js';
|
||||
import { db, initDatabase } from './config/database.js';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import path from 'path';
|
||||
|
||||
// Import routes
|
||||
import authRoute from './routes/auth.js';
|
||||
@ -57,6 +60,14 @@ fastify.register(multipart, {
|
||||
},
|
||||
});
|
||||
|
||||
// Serve static files (uploaded images, etc.) from persistent volume
|
||||
const dataDir = env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data');
|
||||
fastify.register(fastifyStatic, {
|
||||
root: dataDir,
|
||||
prefix: '/static/',
|
||||
decorateReply: false
|
||||
});
|
||||
|
||||
// Decorate fastify with authenticate method
|
||||
fastify.decorate('authenticate', authenticate);
|
||||
|
||||
@ -99,6 +110,8 @@ fastify.setErrorHandler((error, request, reply) => {
|
||||
// Start server
|
||||
const start = async () => {
|
||||
try {
|
||||
// Initialize database before starting server
|
||||
initDatabase();
|
||||
await fastify.listen({ port: env.PORT, host: '0.0.0.0' });
|
||||
console.log(`🚀 Server listening on port ${env.PORT}`);
|
||||
console.log(`📝 Environment: ${env.NODE_ENV}`);
|
||||
|
||||
@ -31,9 +31,8 @@ const authRoute: FastifyPluginAsync = async (fastify) => {
|
||||
reply.setCookie('oauth_state', state, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
// Use HTTPS-based detection to avoid setting Secure on localhost HTTP
|
||||
secure: !!env.FRONTEND_URL && env.FRONTEND_URL.startsWith('https'),
|
||||
sameSite: 'none',
|
||||
secure: true,
|
||||
maxAge: 10 * 60, // 10 minutes
|
||||
});
|
||||
|
||||
@ -59,7 +58,7 @@ const authRoute: FastifyPluginAsync = async (fastify) => {
|
||||
// Verify CSRF state from cookie
|
||||
const expectedState = request.cookies?.oauth_state as string | undefined;
|
||||
if (!expectedState || state !== expectedState) {
|
||||
return reply.code(400).send({ error: 'Invalid state parameter' });
|
||||
return reply.code(400).send({ error: 'Invalid state parameter' });
|
||||
}
|
||||
|
||||
// Clear state cookie
|
||||
@ -122,11 +121,12 @@ const authRoute: FastifyPluginAsync = async (fastify) => {
|
||||
);
|
||||
|
||||
// Also set token as HttpOnly cookie so subsequent API calls authenticate reliably
|
||||
// Cross-site admin (gallus-pub.ch) -> backend (cms.gallus-pub.ch) requires SameSite=None & Secure in production
|
||||
reply.setCookie('token', token, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: !!env.FRONTEND_URL && env.FRONTEND_URL.startsWith('https'),
|
||||
sameSite: (env.NODE_ENV === 'production' ? 'none' : 'lax'),
|
||||
secure: (env.NODE_ENV === 'production') || (!!env.FRONTEND_URL && env.FRONTEND_URL.startsWith('https')),
|
||||
maxAge: 60 * 60 * 24, // 24h
|
||||
});
|
||||
|
||||
|
||||
@ -36,7 +36,15 @@ const reorderBodyJsonSchema = {
|
||||
} as const;
|
||||
|
||||
const eventsRoute: FastifyPluginAsync = async (fastify) => {
|
||||
// List all events (by displayOrder)
|
||||
// PUBLIC: List published events (no auth required)
|
||||
fastify.get('/events/public', async () => {
|
||||
const all = await db.select().from(events)
|
||||
.where(eq(events.isPublished, true))
|
||||
.orderBy(events.displayOrder);
|
||||
return { events: all };
|
||||
});
|
||||
|
||||
// List all events (by displayOrder) - admin only
|
||||
fastify.get('/events', { preHandler: [fastify.authenticate] }, async () => {
|
||||
const all = await db.select().from(events).orderBy(events.displayOrder);
|
||||
return { events: all };
|
||||
|
||||
@ -3,6 +3,9 @@ import { z } from 'zod';
|
||||
import { db } from '../config/database.js';
|
||||
import { galleryImages } from '../db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
// Fastify JSON schema for gallery image body
|
||||
const galleryBodyJsonSchema = {
|
||||
@ -18,7 +21,15 @@ const galleryBodyJsonSchema = {
|
||||
|
||||
const galleryRoute: FastifyPluginAsync = async (fastify) => {
|
||||
|
||||
// List all gallery images
|
||||
// PUBLIC: List published gallery images (no auth required)
|
||||
fastify.get('/gallery/public', async () => {
|
||||
const images = await db.select().from(galleryImages)
|
||||
.where(eq(galleryImages.isPublished, true))
|
||||
.orderBy(galleryImages.displayOrder);
|
||||
return { images };
|
||||
});
|
||||
|
||||
// List all gallery images - admin only
|
||||
fastify.get('/gallery', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
@ -54,6 +65,82 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => {
|
||||
return reply.code(201).send({ image: newImage });
|
||||
});
|
||||
|
||||
// Upload image file (multipart)
|
||||
fastify.post('/gallery/upload', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
try {
|
||||
// Expect a single file field named "file"
|
||||
const file = await (request as any).file();
|
||||
if (!file) {
|
||||
return reply.code(400).send({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
const altText = (file.fields?.altText?.value as string | undefined) || '';
|
||||
const displayOrderRaw = (file.fields?.displayOrder?.value as string | undefined) || '0';
|
||||
const displayOrder = Number.parseInt(displayOrderRaw) || 0;
|
||||
|
||||
const mime = file.mimetype as string | undefined;
|
||||
if (!mime || !mime.startsWith('image/')) {
|
||||
return reply.code(400).send({ error: 'Only image uploads are allowed' });
|
||||
}
|
||||
|
||||
// Prepare directories - use persistent volume for Fly.io
|
||||
const dataDir = process.env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data');
|
||||
const uploadDir = path.join(dataDir, 'images', 'gallery');
|
||||
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
||||
|
||||
// Read uploaded stream into buffer
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of file.file) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
}
|
||||
const inputBuffer = Buffer.concat(chunks);
|
||||
|
||||
// Generate filename
|
||||
const stamp = Date.now().toString(36);
|
||||
const rand = Math.random().toString(36).slice(2, 8);
|
||||
const baseName = `${stamp}-${rand}`;
|
||||
|
||||
// Try to convert to webp and limit size; fallback to original
|
||||
let outBuffer: Buffer | null = null;
|
||||
let outExt = '.webp';
|
||||
try {
|
||||
outBuffer = await sharp(inputBuffer)
|
||||
.rotate()
|
||||
.resize({ width: 1600, withoutEnlargement: true })
|
||||
.webp({ quality: 82 })
|
||||
.toBuffer();
|
||||
} catch {
|
||||
outBuffer = inputBuffer;
|
||||
// naive extension from mimetype
|
||||
const extFromMime = mime.split('/')[1] || 'bin';
|
||||
outExt = '.' + extFromMime.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
||||
}
|
||||
|
||||
const filename = baseName + outExt;
|
||||
const destPath = path.join(uploadDir, filename);
|
||||
fs.writeFileSync(destPath, outBuffer);
|
||||
|
||||
// Public URL (served via /static)
|
||||
const publicUrl = `/static/images/gallery/${filename}`;
|
||||
|
||||
// Store in DB (optional but useful)
|
||||
const [row] = await db.insert(galleryImages).values({
|
||||
imageUrl: publicUrl,
|
||||
altText: altText || filename,
|
||||
displayOrder,
|
||||
isPublished: true,
|
||||
}).returning();
|
||||
|
||||
return reply.code(201).send({ image: row });
|
||||
|
||||
} catch (err) {
|
||||
fastify.log.error({ err }, 'Upload failed');
|
||||
return reply.code(500).send({ error: 'Failed to upload image' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update gallery image
|
||||
fastify.put('/gallery/:id', {
|
||||
schema: {
|
||||
|
||||
16
fly.toml
16
fly.toml
@ -4,14 +4,14 @@ kill_signal = "SIGINT"
|
||||
kill_timeout = 5
|
||||
|
||||
[build]
|
||||
dockerfile = "Dockerfile.fly"
|
||||
dockerfile = "Dockerfile"
|
||||
|
||||
[env]
|
||||
PORT = "3000" # Caddy (serves frontend + proxies /api/*)
|
||||
PORT = "3000"
|
||||
NODE_ENV = "production"
|
||||
BACKEND_PORT = "8080" # Fastify backend will listen here
|
||||
DATABASE_PATH = "/app/data/gallus_cms.db"
|
||||
GIT_WORKSPACE_DIR = "/app/workspace"
|
||||
BACKEND_PORT = "8080"
|
||||
DATABASE_PATH = "/app/data/db/gallus_cms.db"
|
||||
GIT_WORKSPACE_DIR = "/app/data/workspace"
|
||||
|
||||
[http_service]
|
||||
internal_port = 3000
|
||||
@ -46,8 +46,4 @@ kill_timeout = 5
|
||||
|
||||
[[mounts]]
|
||||
source = "gallus_data"
|
||||
destination = "/app/data"
|
||||
|
||||
[[mounts]]
|
||||
source = "gallus_workspace"
|
||||
destination = "/app/workspace"
|
||||
destination = "/app/data"
|
||||
@ -36,7 +36,7 @@ const title = 'Admin';
|
||||
<h2>Authentifizierung</h2>
|
||||
<div id="auth-status" class="muted">Prüfe Anmeldestatus...</div>
|
||||
<div class="row">
|
||||
<a id="login-link" class="btn" href="/api/auth/gitea">Mit Gitea anmelden</a>
|
||||
<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>
|
||||
@ -76,8 +76,11 @@ const title = 'Admin';
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// Base-URL des Backends (separate Subdomain)
|
||||
const API_BASE = 'https://cms.gallus-pub.ch';
|
||||
|
||||
const api = async (path, opts = {}) => {
|
||||
const res = await fetch(path, { credentials: 'include', ...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();
|
||||
@ -107,13 +110,13 @@ const title = 'Admin';
|
||||
loginLink.addEventListener('click', (e) => {
|
||||
try {
|
||||
// Stelle sicher, dass Navigieren erzwungen wird
|
||||
window.location.assign('/api/auth/gitea');
|
||||
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/auth/gitea');
|
||||
window.location.assign(API_BASE + '/api/auth/gitea');
|
||||
});
|
||||
document.getElementById('btn-logout').addEventListener('click', async () => {
|
||||
try { await api('/api/auth/logout', { method: 'POST' }); } catch {}
|
||||
@ -127,7 +130,7 @@ const title = 'Admin';
|
||||
fd.append('file', file);
|
||||
if (altText) fd.append('altText', altText);
|
||||
fd.append('displayOrder', '0');
|
||||
const res = await fetch('/api/gallery/upload', { method: 'POST', body: fd, credentials: 'include' });
|
||||
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();
|
||||
}
|
||||
|
||||
@ -5,82 +5,42 @@ import Welcome from "../components/Welcome.astro";
|
||||
import EventsGrid from "../components/EventsGrid.astro";
|
||||
import Drinks from "../components/Drinks.astro";
|
||||
import ImageCarousel from "../components/ImageCarousel.astro";
|
||||
import Contact from "../components/Contact.astro";
|
||||
import About from "../components/About.astro";
|
||||
|
||||
const events = [
|
||||
{
|
||||
image: "/images/events/event_karaoke.jpg",
|
||||
title: "Karaoke",
|
||||
date: "Mittwoch - Samstag",
|
||||
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>
|
||||
`,
|
||||
},
|
||||
{
|
||||
image: "/images/events/event_pub-quiz.jpg",
|
||||
title: "Pub Quiz",
|
||||
date: "Jeden Freitag",
|
||||
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>
|
||||
*zum mitmachen minimum 1 Getränk konsumieren oder 5CHF
|
||||
`,
|
||||
},
|
||||
{
|
||||
image: "/images/events/event_schlager-karaoke.jpeg",
|
||||
title: "Schlager Hüttenzauber Karaoke",
|
||||
date: "27. November - 19:00 Uhr",
|
||||
description: `
|
||||
Ab 19:00 Uhr Eintritt ist Frei! Reservieren unter <a href="tel:+41772322770">077 232 27 70</a>
|
||||
`,
|
||||
},
|
||||
{
|
||||
image: "/images/events/event_advents-kalender.jpeg",
|
||||
title: "Adventskalender",
|
||||
date: "03. Dezember - 20. Dezember 2025",
|
||||
description: `
|
||||
Jeden Tag neue Überraschungen! Check unsere Social Media Stories!
|
||||
`,
|
||||
},
|
||||
{
|
||||
image: "/images/events/event_santa_karaoke.jpeg",
|
||||
title: "Santa Karaoke-Party",
|
||||
date: "06. Dezember 2025",
|
||||
description: `
|
||||
🤶🏻🎅🏻Komme als Weihnachts-Mann/-Frau und bekomme einen Shot auf's Haus!🤶🏻🎅🏻 `,
|
||||
},
|
||||
{
|
||||
image: "/images/events/event_ferien.jpeg",
|
||||
title: "Weihnachtsferien",
|
||||
date: "21. Dezember 2025 - 01. Januar 2026",
|
||||
description: `
|
||||
Wir sind ab 02.01.2026 wieder wie gewohnt für euch da! 🍀. <br> Für Anfragen WA <a href="tel:+41772322770">077 232 27 70</a> Antwort innerhalb 48h
|
||||
`,
|
||||
},
|
||||
{
|
||||
image: "/images/events/event_neujahrs-apero.jpeg",
|
||||
title: "Neujahrs-Apero",
|
||||
date: "02. Januar 2026 - 18:00-20:00 Uhr",
|
||||
description: `
|
||||
|
||||
`,
|
||||
},
|
||||
const API_BASE = 'https://cms.gallus-pub.ch';
|
||||
|
||||
];
|
||||
// Fetch events from backend API
|
||||
let events = [];
|
||||
try {
|
||||
const eventsResponse = await fetch(`${API_BASE}/api/events/public`);
|
||||
if (eventsResponse.ok) {
|
||||
const eventsData = await eventsResponse.json();
|
||||
events = (eventsData.events || []).map((ev: any) => ({
|
||||
image: `${API_BASE}${ev.imageUrl}`,
|
||||
title: ev.title,
|
||||
date: ev.date,
|
||||
description: ev.description
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch events:', error);
|
||||
}
|
||||
|
||||
const images = [
|
||||
{ src: "/images/gallery/Gallery7.png", alt: "Siebtes Bild" },
|
||||
{ src: "/images/gallery/Gallery8.png", alt: "Achtes Bild" },
|
||||
{ src: "/images/gallery/Gallery9.png", alt: "Neuntes Bild" },
|
||||
{ src: "/images/gallery/Gallery6.png", alt: "Sechstes Bild" },
|
||||
{ src: "/images/gallery/Gallery1.png", alt: "Erstes Bild" },
|
||||
{ src: "/images/gallery/Gallery2.png", alt: "Zweites Bild" },
|
||||
{ src: "/images/gallery/Gallery3.png", alt: "Drittes Bild" },
|
||||
{ src: "/images/gallery/Gallery4.png", alt: "Viertes Bild" },
|
||||
{ src: "/images/gallery/Gallery5.png", alt: "Fünftes Bild" },
|
||||
];
|
||||
// Fetch gallery images from backend API
|
||||
let images = [];
|
||||
try {
|
||||
const galleryResponse = await fetch(`${API_BASE}/api/gallery/public`);
|
||||
if (galleryResponse.ok) {
|
||||
const galleryData = await galleryResponse.json();
|
||||
images = (galleryData.images || []).map((img: any) => ({
|
||||
src: `${API_BASE}${img.imageUrl}`,
|
||||
alt: img.altText
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch gallery:', error);
|
||||
}
|
||||
---
|
||||
|
||||
<Layout>
|
||||
|
||||
Reference in New Issue
Block a user