47 Commits

Author SHA1 Message Date
20feee84a6 Jetzt wird in der Event-Liste das Bild als Vorschau angezeigt mit dem Event-Titel als Alt-Text
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-17 20:54:20 +01:00
bf7e38ba2d 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.
2025-12-17 20:47:38 +01:00
d0101b2974 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-15 16:38:55 +00:00
75f0c41e5c Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-15 16:34:32 +00:00
ffadf378f9 Update events
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 16:32:41 +00:00
5e7425cadf Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 21:06:33 +00:00
7a067f60ff Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 21:03:20 +00:00
980200f963 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 20:50:25 +00:00
de278fab29 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 20:45:37 +00:00
160d384143 Update events
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-14 20:45:04 +00:00
f440cbb7f3 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 20:40:57 +00:00
72faefc88d Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 20:36:20 +00:00
e4ada94390 Update events
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-14 20:34:31 +00:00
4eab0e6dd2 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-14 20:31:47 +00:00
6222d5f19c Revert "images fixing with database saves"
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This reverts commit c45e054787.
2025-12-09 20:26:55 +01:00
c45e054787 images fixing with database saves
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 20:12:08 +01:00
8eb2be8628 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 18:42:28 +00:00
3aafda5f70 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 18:35:29 +00:00
f27e9a0027 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 18:27:38 +00:00
921d2527e0 Merge remote-tracking branch 'origin/main'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 19:25:24 +01:00
901223fcd9 events and gallery backend fix 2025-12-09 19:25:17 +01:00
357d5ba077 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 18:22:09 +00:00
5d0c0a0b17 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 18:15:34 +00:00
cb483d8715 picture test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 19:08:55 +01:00
3fd5dcf6dd picture test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 19:05:37 +01:00
86c2e4e306 picture test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 19:02:06 +01:00
0f16b944bc picture test 2025-12-09 19:02:06 +01:00
10752a7337 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:57:06 +00:00
901b6a11db Update events 2025-12-09 17:54:50 +00:00
10192e2627 feat: Add vips dependencies for sharp in backend Dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation
2025-12-09 18:37:30 +01:00
b1d2f8b441 feat: Add vips dependencies for sharp in backend Dockerfile
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation
2025-12-09 18:35:57 +01:00
5215765588 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:31:37 +00:00
c368be5a27 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:27:33 +00:00
54de8e36e2 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:25:00 +00:00
57d7d48d5d Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:16:54 +00:00
b2f04dc726 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 17:13:20 +00:00
6ea6a58532 feat: Add vips dependencies for sharp in backend Dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation
2025-12-09 18:10:25 +01:00
f00a2ef934 feat: Add vips dependencies for sharp in backend Dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation
2025-12-09 18:06:26 +01:00
ccc5c028ba feat: Add vips dependencies for sharp in backend Dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation
2025-12-09 18:00:32 +01:00
7c96a15c2e feat: Add vips dependencies for sharp in backend Dockerfile
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Added `vips-dev` and `vips` to build and runtime dependencies.
- Updated installation process to ensure compilation of native modules during build.
2025-12-09 17:58:21 +01:00
7bfb777a74 feat: Add gallery management and dynamic API-based data loading
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Introduced a gallery management section in `admin.astro` for uploading, listing, and deleting gallery images.
- Added dynamic fetching of events and gallery images from the backend in `index.astro`.
- Updated authentication to handle gallery-related UI visibility and actions.
2025-12-09 17:42:27 +01:00
9c3b4be79d Add event image upload endpoint and refactor image upload handling
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Introduced `/events/upload` endpoint for securely uploading and processing event images.
- Added image validation, resizing, and conversion to WebP with fallback support for original formats.
- Updated `uploadImage` to `uploadEventImage` and introduced `uploadGalleryImage` in `admin.astro`.
2025-12-09 17:34:06 +01:00
89640a3372 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 16:29:43 +00:00
4ed0016be9 Update events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 15:55:21 +00:00
745888d01b Merge remote-tracking branch 'origin/main'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 16:51:32 +01:00
febd5a886c feat: Add production migration script for Fly.io deployment
- Create standalone migration script that works in production
- Include migration script and images in Docker build
- Images will be copied to /app/data/images on container start
- Can be run with: node migrate-production.js
2025-12-09 16:50:48 +01:00
25305c4aad Update events
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-09 15:50:36 +00:00
40 changed files with 3252 additions and 49 deletions

View File

@ -5,8 +5,8 @@ FROM node:20-alpine AS builder
WORKDIR /app
# Install build dependencies for native modules (better-sqlite3)
RUN apk add --no-cache python3 make g++
# Install build dependencies for native modules (better-sqlite3, sharp)
RUN apk add --no-cache python3 make g++ vips-dev
# Install dependencies
COPY package*.json ./
@ -24,16 +24,28 @@ FROM node:20-alpine
WORKDIR /app
# Install runtime dependencies (git for simple-git, sqlite3 CLI tool)
RUN apk add --no-cache git sqlite
# Install runtime dependencies (git for simple-git, sqlite3 CLI tool, vips for sharp)
# Note: python3, make, g++ are needed for native module compilation
RUN apk add --no-cache git sqlite vips vips-dev python3 make g++
# Copy production dependencies from builder (already compiled native modules)
COPY --from=builder /app/node_modules ./node_modules
# Copy package files first
COPY --from=builder /app/package*.json ./
# Install all production dependencies and rebuild sharp for linuxmusl-x64
RUN npm ci --omit=dev || npm install --production && \
npm rebuild sharp
# Clean up build dependencies after installation to reduce image size
RUN apk del python3 make g++ vips-dev
# Copy built files from builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/src/db/migrations ./dist/db/migrations
# Copy migration script and migrated images
COPY --from=builder /app/migrate-production.js ./migrate-production.js
COPY --from=builder /app/data/images ./data/images
# Create directories
RUN mkdir -p /app/workspace /app/data
@ -56,4 +68,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:8080/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Run DB migrations if present, then start application
CMD ["/bin/sh", "-lc", "[ -f dist/migrate.js ] && node dist/migrate.js || true; node dist/index.js"]
CMD ["/bin/sh", "-lc", "mkdir -p /app/data/images/events /app/data/images/gallery && [ -f dist/migrate.js ] && node dist/migrate.js || true; node dist/index.js"]

View File

@ -0,0 +1,183 @@
// Production migration script - can be run directly with node
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
// Database schema
const events = sqliteTable('events', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
title: text('title').notNull(),
date: text('date').notNull(),
description: text('description').notNull(),
imageUrl: text('image_url').notNull(),
displayOrder: integer('display_order').notNull(),
isPublished: integer('is_published', { mode: 'boolean' }).default(true),
createdAt: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
updatedAt: integer('updated_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
});
const galleryImages = sqliteTable('gallery_images', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
imageUrl: text('image_url').notNull(),
altText: text('alt_text').notNull(),
displayOrder: integer('display_order').notNull(),
isPublished: integer('is_published', { mode: 'boolean' }).default(true),
createdAt: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
});
// Old events data
const oldEvents = [
{
title: "Karaoke",
date: "2025-12-31",
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>`,
imageUrl: "/images/events/event_karaoke.webp",
displayOrder: 0,
},
{
title: "Pub Quiz",
date: "2025-12-31",
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`,
displayOrder: 1,
},
{
title: "Schlager Hüttenzauber Karaoke",
date: "2025-11-27",
description: `Ab 19:00 Uhr Eintritt ist Frei! Reservieren unter <a href="tel:+41772322770">077 232 27 70</a>`,
imageUrl: "/images/events/event_schlager-karaoke.webp",
displayOrder: 2,
},
{
title: "Adventskalender",
date: "2025-12-20",
description: `Jeden Tag neue Überraschungen! Check unsere Social Media Stories!`,
imageUrl: "/images/events/event_advents-kalender.webp",
displayOrder: 3,
},
{
title: "Santa Karaoke-Party",
date: "2025-12-06",
description: `🤶🏻🎅🏻Komme als Weihnachts-Mann/-Frau und bekomme einen Shot auf's Haus!🤶🏻🎅🏻`,
imageUrl: "/images/events/event_santa_karaoke.webp",
displayOrder: 4,
},
{
title: "Weihnachtsferien",
date: "2025-12-21",
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`,
imageUrl: "/images/events/event_ferien.webp",
displayOrder: 5,
},
{
title: "Neujahrs-Apero",
date: "2026-01-02",
description: `18:00-20:00 Uhr`,
imageUrl: "/images/events/event_neujahrs-apero.webp",
displayOrder: 6,
},
];
// Old gallery images
const oldGalleryImages = [
{ imageUrl: "/images/gallery/Gallery7.webp", alt: "Gallery 7", order: 0 },
{ imageUrl: "/images/gallery/Gallery8.webp", alt: "Gallery 8", order: 1 },
{ imageUrl: "/images/gallery/Gallery9.webp", alt: "Gallery 9", order: 2 },
{ imageUrl: "/images/gallery/Gallery6.webp", alt: "Gallery 6", order: 3 },
{ imageUrl: "/images/gallery/Gallery1.webp", alt: "Gallery 1", order: 4 },
{ imageUrl: "/images/gallery/Gallery2.webp", alt: "Gallery 2", order: 5 },
{ imageUrl: "/images/gallery/Gallery3.webp", alt: "Gallery 3", order: 6 },
{ imageUrl: "/images/gallery/Gallery4.webp", alt: "Gallery 4", order: 7 },
{ imageUrl: "/images/gallery/Gallery5.webp", alt: "Gallery 5", order: 8 },
];
async function main() {
console.log('=== Production Migration Script ===\n');
const dbPath = process.env.DATABASE_PATH || '/app/data/gallus_cms.db';
console.log('Database path:', dbPath);
// Check if database exists
if (!fs.existsSync(dbPath)) {
console.error('ERROR: Database not found at:', dbPath);
console.error('Please ensure the backend has been started at least once to create the database.');
process.exit(1);
}
// Check if images exist
const dataDir = process.env.GIT_WORKSPACE_DIR || '/app/data';
const eventsDir = path.join(dataDir, 'images', 'events');
const galleryDir = path.join(dataDir, 'images', 'gallery');
console.log('Events images directory:', eventsDir);
console.log('Gallery images directory:', galleryDir);
if (!fs.existsSync(eventsDir)) {
console.error('ERROR: Events images directory not found:', eventsDir);
process.exit(1);
}
if (!fs.existsSync(galleryDir)) {
console.error('ERROR: Gallery images directory not found:', galleryDir);
process.exit(1);
}
// List available images
console.log('\nAvailable event images:', fs.readdirSync(eventsDir));
console.log('Available gallery images:', fs.readdirSync(galleryDir));
// Connect to database
const sqlite = new Database(dbPath);
const db = drizzle(sqlite);
console.log('\n=== Migrating Events ===\n');
for (const event of oldEvents) {
try {
const [newEvent] = await db.insert(events).values({
title: event.title,
date: event.date,
description: event.description,
imageUrl: event.imageUrl,
displayOrder: event.displayOrder,
isPublished: true,
}).returning();
console.log(`✓ Migrated event: ${newEvent.title}`);
} catch (error) {
console.error(`✗ Failed to migrate event "${event.title}":`, error.message);
}
}
console.log('\n=== Migrating Gallery Images ===\n');
for (const img of oldGalleryImages) {
try {
const [newImage] = await db.insert(galleryImages).values({
imageUrl: img.imageUrl,
altText: img.alt,
displayOrder: img.order,
isPublished: true,
}).returning();
console.log(`✓ Migrated gallery image: ${newImage.altText}`);
} catch (error) {
console.error(`✗ Failed to migrate gallery image "${img.alt}":`, error.message);
}
}
sqlite.close();
console.log('\n✓ Migration completed successfully!');
console.log('\nYou can verify the migration by visiting:');
console.log('- Frontend: https://gallus-pub.ch/');
console.log('- Admin: https://gallus-pub.ch/admin');
}
main().catch(error => {
console.error('\n✗ Migration failed:', error);
process.exit(1);
});

2704
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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())`),
});

View File

@ -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 () => {

View File

@ -2,6 +2,8 @@ import { FastifyPluginAsync } from 'fastify';
import { db } from '../config/database.js';
import { events } from '../db/schema.js';
import { eq } from 'drizzle-orm';
import fs from 'fs';
import path from 'path';
// Fastify JSON schema for event body
const eventBodyJsonSchema = {
@ -74,6 +76,73 @@ const eventsRoute: FastifyPluginAsync = async (fastify) => {
return { event: row };
});
// Upload event image file (multipart)
fastify.post('/events/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 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, 'public', 'images', 'events');
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 {
// Lazy load sharp only when needed
const sharp = (await import('sharp')).default;
outBuffer = await sharp(inputBuffer)
.rotate()
.resize({ width: 1600, withoutEnlargement: true })
.webp({ quality: 82 })
.toBuffer();
} catch (err) {
fastify.log.warn({ err }, 'Sharp processing failed, using original image');
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 = `/images/events/${filename}`;
return reply.code(201).send({ imageUrl: publicUrl });
} catch (err) {
fastify.log.error({ err }, 'Upload failed');
return reply.code(500).send({ error: 'Failed to upload image' });
}
});
// Delete event
fastify.delete('/events/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
const { id } = request.params as { id: string };

View File

@ -5,7 +5,6 @@ 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 = {
@ -87,7 +86,7 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => {
// 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');
const uploadDir = path.join(dataDir, 'public', 'images', 'gallery');
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
// Read uploaded stream into buffer
@ -106,12 +105,15 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => {
let outBuffer: Buffer | null = null;
let outExt = '.webp';
try {
// Lazy load sharp only when needed
const sharp = (await import('sharp')).default;
outBuffer = await sharp(inputBuffer)
.rotate()
.resize({ width: 1600, withoutEnlargement: true })
.webp({ quality: 82 })
.toBuffer();
} catch {
} catch (err) {
fastify.log.warn({ err }, 'Sharp processing failed, using original image');
outBuffer = inputBuffer;
// naive extension from mimetype
const extFromMime = mime.split('/')[1] || 'bin';
@ -123,7 +125,7 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => {
fs.writeFileSync(destPath, outBuffer);
// Public URL (served via /static)
const publicUrl = `/static/images/gallery/${filename}`;
const publicUrl = `/images/gallery/${filename}`;
// Store in DB (optional but useful)
const [row] = await db.insert(galleryImages).values({

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 KiB

View File

@ -51,7 +51,6 @@ const title = 'Admin';
<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>
<label>Alt-Text<input id="ev-alt" placeholder="Bildbeschreibung" /></label>
<button id="btn-create-ev">Event anlegen</button>
<div id="ev-create-msg" class="muted"></div>
</div>
@ -68,6 +67,41 @@ const title = 'Admin';
</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-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>
@ -92,15 +126,21 @@ const title = 'Admin';
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-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');
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-banner').style.display = 'none';
document.getElementById('sec-publish').style.display = 'none';
}
}
@ -125,7 +165,15 @@ const title = 'Admin';
});
// ========== Events & Publish ==========
async function uploadImage(file, altText) {
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);
@ -175,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>`;
@ -230,14 +278,13 @@ const title = 'Admin';
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 alt = (document.getElementById('ev-alt')).value.trim();
const msg = document.getElementById('ev-create-msg');
msg.textContent = 'Lade Bild hoch...';
try {
let imageUrl = '';
if (file) {
const up = await uploadImage(file, alt || title);
imageUrl = up?.image?.imageUrl || '';
const up = await uploadEventImage(file);
imageUrl = up?.imageUrl || '';
}
msg.textContent = 'Lege Event an...';
await api('/api/events', {
@ -249,7 +296,6 @@ const title = 'Admin';
(document.getElementById('ev-date')).value = '';
(document.getElementById('ev-desc')).value = '';
(document.getElementById('ev-file')).value = '';
(document.getElementById('ev-alt')).value = '';
await loadEvents();
} catch(e){ msg.textContent = 'Fehler: '+e.message }
});
@ -288,6 +334,147 @@ const title = 'Admin';
} 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';
card.innerHTML = `
<img src="${API_BASE}${img.imageUrl}" 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 }
});
// ========== 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";
@ -8,43 +9,75 @@ import ImageCarousel from "../components/ImageCarousel.astro";
import Contact from "../components/Contact.astro";
import About from "../components/About.astro";
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
}));
const events = [
{
image: "/images/events/mj7dj1ko-mtnbg6.jpeg",
title: "Karaoke",
date: "2025-12-01",
description: `
Von Mittwoch bis Samstag kannst du deine Stimme zum Besten geben. Du singst gerne, aber lieber für dich? Dann kannst du den 2. OG auch privat mieten. 🍀 WA 077 232 27 70
`,
},
{
image: "/images/events/mj67800i-6ng82x.jpeg",
title: "Pub Quiz",
date: "2025-12-02",
description: `
Jeden Freitag 20:00Uhr-ca 21:30Uhr.
Plätze sind begrenzt! Jetzt reservieren unter 🍀WA 077 232 27 70
`,
},
{
image: "/images/events/mj67ssjo-sp3i0e.jpeg",
title: "Adventskalender",
date: "2025-12-03",
description: `
Jeden Tag neue Überraschungen!
Check unsere Social Media Stories oder komm am besten gleich vorbei!
`,
},
{
image: "/images/events/miyxej2c-7l8end.jpeg",
title: "Ferien Flyer",
date: "2026-01-02",
description: `
Wir sind ab 02.01.2026 wieder wie gewohnt für euch da! 🍀Für Anfragen WA 077 232 27 70 Antwort innerhalb 48h
`,
},
{
image: "/images/events/miyxfgm1-jg8gqi.jpeg",
title: "New Year Flyer",
date: "2026-01-02",
description: `
-
`,
},
{
image: "/images/events/mj7donky-md8jp5.jpeg",
title: "Celtik Folk Night",
date: "2026-01-29",
description: `
Celtic Folk Night im Gallus Pub!✨🌿20:30Uhr Eintritt ist Frei/Hutkollekte. Reservation via WA 077 232 27 70
`,
}
} catch (error) {
console.error('Failed to fetch events:', error);
}
];
// 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);
}
const images = [
{ src: "/images/gallery/miywxkwh-m4xaww.webp", alt: "miywxkwh-m4xaww.webp" },
{ src: "/images/gallery/miyxgbqr-n3zzrg.png", alt: "miyxgbqr-n3zzrg.png" },
{ src: "/images/gallery/miyxgfh1-c7zawh.png", alt: "miyxgfh1-c7zawh.png" },
{ src: "/images/gallery/miyxgjff-wjtyim.png", alt: "miyxgjff-wjtyim.png" },
{ src: "/images/gallery/miyxgn6h-jsaltu.png", alt: "miyxgn6h-jsaltu.png" },
{ src: "/images/gallery/mj67l5x3-pdasw8.jpeg", alt: "mj67l5x3-pdasw8.jpeg" },
{ src: "/images/gallery/mj67mw2z-3pd81q.jpeg", alt: "mj67mw2z-3pd81q.jpeg" },
{ src: "/images/gallery/mj67nwjs-6oaijj.jpeg", alt: "mj67nwjs-6oaijj.jpeg" },
{ src: "/images/gallery/mj67ove6-el3pf7.png", alt: "mj67ove6-el3pf7.png" }
];
---
<Layout>
<Hero id="hero" />
<Banner />
<Welcome id="welcome" />
<EventsGrid id="events" events={events} />
<ImageCarousel id="gallery" images={images} />