diff --git a/backend/src/routes/events.ts b/backend/src/routes/events.ts index 8e1553e..997a21e 100644 --- a/backend/src/routes/events.ts +++ b/backend/src/routes/events.ts @@ -2,6 +2,9 @@ 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'; +import sharp from 'sharp'; // Fastify JSON schema for event body const eventBodyJsonSchema = { @@ -74,6 +77,70 @@ 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, '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 { + 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/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 }; diff --git a/src/pages/admin.astro b/src/pages/admin.astro index 8570387..99c7bd4 100644 --- a/src/pages/admin.astro +++ b/src/pages/admin.astro @@ -51,7 +51,6 @@ const title = 'Admin'; -
@@ -125,7 +124,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); @@ -230,14 +237,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 +255,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 } });