From c45e054787d9bc5f8dd7931747291e4f1b84cca5 Mon Sep 17 00:00:00 2001 From: Kenzo Date: Tue, 9 Dec 2025 20:12:08 +0100 Subject: [PATCH] images fixing with database saves --- backend/Dockerfile | 35 +- backend/src/db/schema.ts | 8 +- backend/src/routes/events.ts | 287 +++++++++++--- backend/src/routes/gallery.ts | 191 ++++++---- src/pages/admin.astro | 693 +++++++++++++++++----------------- 5 files changed, 718 insertions(+), 496 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index c44d951..5a58a83 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -5,67 +5,56 @@ FROM node:20-alpine AS builder WORKDIR /app -# Install build dependencies for native modules (better-sqlite3, sharp) RUN apk add --no-cache python3 make g++ vips-dev -# Install dependencies COPY package*.json ./ -# Use npm ci when lockfile exists, fallback to npm install for local/dev RUN npm ci || npm install -# Copy source COPY . . -# Build TypeScript +RUN npm run db:generate || true + RUN npm run build + # Stage 2: Production FROM node:20-alpine WORKDIR /app -# 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 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 built files 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 -# Ensure proper permissions RUN chown -R node:node /app - -# Switch to non-root user USER node -# Expose port EXPOSE 8080 -# Set environment ENV NODE_ENV=production ENV PORT=8080 ENV DATABASE_PATH=/app/data/gallus_cms.db -# Health check -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", "mkdir -p /app/data/images/events /app/data/images/gallery && [ -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; \ +echo 'Running Drizzle migrations...'; \ +npx drizzle-kit generate:sqlite || true; \ +npx drizzle-kit migrate || true; \ +node dist/index.js \ +"] diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index a72ea2c..f9e5bea 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -20,7 +20,8 @@ export const events = sqliteTable('events', { title: text('title').notNull(), date: text('date').notNull(), description: text('description').notNull(), - imageUrl: text('image_url').notNull(), + imageData: text('image_data').notNull(), // base64 encoded image + mimeType: text('mime_type').notNull(), // e.g. 'image/webp', 'image/jpeg' displayOrder: integer('display_order').notNull(), isPublished: integer('is_published', { mode: 'boolean' }).default(true), createdAt: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`), @@ -30,7 +31,8 @@ export const events = sqliteTable('events', { // Gallery images table export const galleryImages = sqliteTable('gallery_images', { id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), - imageUrl: text('image_url').notNull(), + imageData: text('image_data').notNull(), // base64 encoded image + mimeType: text('mime_type').notNull(), // e.g. 'image/webp', 'image/jpeg' altText: text('alt_text').notNull(), displayOrder: integer('display_order').notNull(), isPublished: integer('is_published', { mode: 'boolean' }).default(true), @@ -59,4 +61,4 @@ export const publishHistory = sqliteTable('publish_history', { commitHash: text('commit_hash'), commitMessage: text('commit_message'), publishedAt: integer('published_at', { mode: 'timestamp' }).default(sql`(unixepoch())`), -}); +}); \ No newline at end of file diff --git a/backend/src/routes/events.ts b/backend/src/routes/events.ts index 6cdb352..53829e3 100644 --- a/backend/src/routes/events.ts +++ b/backend/src/routes/events.ts @@ -2,20 +2,18 @@ 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 = { type: 'object', - required: ['title', 'date', 'description', 'imageUrl', 'displayOrder'], + required: ['title', 'date', 'description', 'displayOrder'], properties: { title: { type: 'string', minLength: 1, maxLength: 200 }, date: { type: 'string', minLength: 1, maxLength: 100 }, description: { type: 'string', minLength: 1 }, - imageUrl: { type: 'string', minLength: 1 }, displayOrder: { type: 'integer', minimum: 0 }, isPublished: { type: 'boolean' }, + imageData: { type: 'string' }, + mimeType: { type: 'string' }, }, } as const; @@ -38,50 +36,126 @@ const reorderBodyJsonSchema = { } as const; const eventsRoute: FastifyPluginAsync = async (fastify) => { + // 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 }; + const all = await db.select({ + id: events.id, + title: events.title, + date: events.date, + description: events.description, + displayOrder: events.displayOrder, + isPublished: events.isPublished, + }).from(events) + .where(eq(events.isPublished, true)) + .orderBy(events.displayOrder); + + return { + events: all.map(ev => ({ + ...ev, + imageUrl: `/api/events/${ev.id}/image`, + })) + }; }); - // List all events (by displayOrder) - admin only + // Serve event image + fastify.get('/events/:id/image', async (request, reply) => { + const { id } = request.params as { id: string }; + const [event] = await db.select({ + imageData: events.imageData, + mimeType: events.mimeType, + }).from(events).where(eq(events.id, id)).limit(1); + + if (!event || !event.imageData) { + return reply.code(404).send({ error: 'Image not found' }); + } + + const buffer = Buffer.from(event.imageData, 'base64'); + return reply + .header('Content-Type', event.mimeType || 'image/webp') + .header('Cache-Control', 'public, max-age=31536000') + .send(buffer); + }); + + // List all events - admin only fastify.get('/events', { preHandler: [fastify.authenticate] }, async () => { - const all = await db.select().from(events).orderBy(events.displayOrder); - return { events: all }; + const all = await db.select({ + id: events.id, + title: events.title, + date: events.date, + description: events.description, + displayOrder: events.displayOrder, + isPublished: events.isPublished, + }).from(events).orderBy(events.displayOrder); + + return { + events: all.map(ev => ({ + ...ev, + imageUrl: `/api/events/${ev.id}/image`, + })) + }; }); // Get single event fastify.get('/events/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => { const { id } = request.params as { id: string }; - const rows = await db.select().from(events).where(eq(events.id, id)).limit(1); - if (rows.length === 0) return reply.code(404).send({ error: 'Event not found' }); - return { event: rows[0] }; + const [event] = await db.select({ + id: events.id, + title: events.title, + date: events.date, + description: events.description, + displayOrder: events.displayOrder, + isPublished: events.isPublished, + }).from(events).where(eq(events.id, id)).limit(1); + + if (!event) { + return reply.code(404).send({ error: 'Event not found' }); + } + + return { + event: { + ...event, + imageUrl: `/api/events/${event.id}/image`, + } + }; }); // Create event - fastify.post('/events', { schema: { body: eventBodyJsonSchema }, preHandler: [fastify.authenticate] }, async (request, reply) => { + fastify.post('/events', { + schema: { body: eventBodyJsonSchema }, + preHandler: [fastify.authenticate], + }, async (request, reply) => { const data = request.body as any; - const [row] = await db.insert(events).values(data).returning(); - return reply.code(201).send({ event: row }); + const [row] = await db.insert(events).values({ + title: data.title, + date: data.date, + description: data.description, + displayOrder: data.displayOrder, + isPublished: data.isPublished ?? true, + imageData: data.imageData || '', + mimeType: data.mimeType || '', + }).returning({ + id: events.id, + title: events.title, + date: events.date, + description: events.description, + displayOrder: events.displayOrder, + isPublished: events.isPublished, + }); + + return reply.code(201).send({ + event: { + ...row, + imageUrl: `/api/events/${row.id}/image`, + } + }); }); - // Update event - fastify.put('/events/:id', { schema: { body: eventBodyJsonSchema }, preHandler: [fastify.authenticate] }, async (request, reply) => { - const { id } = request.params as { id: string }; - const data = request.body as any; - const [row] = await db.update(events).set({ ...data, updatedAt: new Date() }).where(eq(events.id, id)).returning(); - if (!row) return reply.code(404).send({ error: 'Event not found' }); - return { event: row }; - }); - - // Upload event image file (multipart) + // Upload event image (returns base64 data) 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' }); @@ -92,11 +166,6 @@ const eventsRoute: FastifyPluginAsync = async (fastify) => { 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) { @@ -104,38 +173,25 @@ const eventsRoute: FastifyPluginAsync = async (fastify) => { } 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}`; + // Process image and convert to base64 + let imageData: string; + let mimeType = 'image/webp'; - // 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(); + const processedBuffer = await sharp(inputBuffer) + .rotate() + .resize({ width: 1600, withoutEnlargement: true }) + .webp({ quality: 82 }) + .toBuffer(); + imageData = processedBuffer.toString('base64'); } 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(); + imageData = inputBuffer.toString('base64'); + mimeType = mime; } - 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 }); + return reply.code(201).send({ imageData, mimeType }); } catch (err) { fastify.log.error({ err }, 'Upload failed'); @@ -143,6 +199,110 @@ const eventsRoute: FastifyPluginAsync = async (fastify) => { } }); + // Update event + fastify.put('/events/:id', { + schema: { body: eventBodyJsonSchema }, + preHandler: [fastify.authenticate], + }, async (request, reply) => { + const { id } = request.params as { id: string }; + const data = request.body as any; + + const updateData: any = { + title: data.title, + date: data.date, + description: data.description, + displayOrder: data.displayOrder, + isPublished: data.isPublished, + updatedAt: new Date(), + }; + + // Only update image if provided + if (data.imageData) { + updateData.imageData = data.imageData; + updateData.mimeType = data.mimeType || 'image/webp'; + } + + const [row] = await db.update(events) + .set(updateData) + .where(eq(events.id, id)) + .returning({ + id: events.id, + title: events.title, + date: events.date, + description: events.description, + displayOrder: events.displayOrder, + isPublished: events.isPublished, + }); + + if (!row) { + return reply.code(404).send({ error: 'Event not found' }); + } + + return { + event: { + ...row, + imageUrl: `/api/events/${row.id}/image`, + } + }; + }); + + // Update event image only + fastify.put('/events/:id/image', { + preHandler: [fastify.authenticate], + }, async (request, reply) => { + const { id } = request.params as { id: string }; + + try { + 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' }); + } + + const chunks: Buffer[] = []; + for await (const chunk of file.file) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + const inputBuffer = Buffer.concat(chunks); + + let imageData: string; + let mimeType = 'image/webp'; + + try { + const sharp = (await import('sharp')).default; + const processedBuffer = await sharp(inputBuffer) + .rotate() + .resize({ width: 1600, withoutEnlargement: true }) + .webp({ quality: 82 }) + .toBuffer(); + imageData = processedBuffer.toString('base64'); + } catch (err) { + fastify.log.warn({ err }, 'Sharp processing failed, using original image'); + imageData = inputBuffer.toString('base64'); + mimeType = mime; + } + + const [row] = await db.update(events) + .set({ imageData, mimeType, updatedAt: new Date() }) + .where(eq(events.id, id)) + .returning({ id: events.id }); + + if (!row) { + return reply.code(404).send({ error: 'Event not found' }); + } + + return { message: 'Image updated', imageUrl: `/api/events/${id}/image` }; + + } catch (err) { + fastify.log.error({ err }, 'Image update failed'); + return reply.code(500).send({ error: 'Failed to update image' }); + } + }); + // Delete event fastify.delete('/events/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => { const { id } = request.params as { id: string }; @@ -151,8 +311,11 @@ const eventsRoute: FastifyPluginAsync = async (fastify) => { return { message: 'Event deleted successfully' }; }); - // Reorder events (synchronous transaction for better-sqlite3) - fastify.put('/events/reorder', { schema: { body: reorderBodyJsonSchema }, preHandler: [fastify.authenticate] }, async (request) => { + // Reorder events + fastify.put('/events/reorder', { + schema: { body: reorderBodyJsonSchema }, + preHandler: [fastify.authenticate], + }, async (request) => { const { orders } = request.body as { orders: Array<{ id: string; displayOrder: number }> }; db.transaction((tx: any) => { for (const { id, displayOrder } of orders) { @@ -163,4 +326,4 @@ const eventsRoute: FastifyPluginAsync = async (fastify) => { }); }; -export default eventsRoute; +export default eventsRoute; \ No newline at end of file diff --git a/backend/src/routes/gallery.ts b/backend/src/routes/gallery.ts index e6695af..c7645b3 100644 --- a/backend/src/routes/gallery.ts +++ b/backend/src/routes/gallery.ts @@ -1,17 +1,12 @@ import { FastifyPluginAsync } from 'fastify'; -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'; -// Fastify JSON schema for gallery image body const galleryBodyJsonSchema = { type: 'object', - required: ['imageUrl', 'altText', 'displayOrder'], + required: ['altText', 'displayOrder'], properties: { - imageUrl: { type: 'string', minLength: 1 }, altText: { type: 'string', minLength: 1, maxLength: 200 }, displayOrder: { type: 'integer', minimum: 0 }, isPublished: { type: 'boolean' }, @@ -22,46 +17,99 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { // 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 }; + const images = await db.select({ + id: galleryImages.id, + altText: galleryImages.altText, + displayOrder: galleryImages.displayOrder, + isPublished: galleryImages.isPublished, + }).from(galleryImages) + .where(eq(galleryImages.isPublished, true)) + .orderBy(galleryImages.displayOrder); + + // Return with generated URLs pointing to the image endpoint + return { + images: images.map(img => ({ + ...img, + imageUrl: `/api/gallery/${img.id}/image`, + })) + }; + }); + + // Serve image data + fastify.get('/gallery/:id/image', async (request, reply) => { + const { id } = request.params as { id: string }; + const [image] = await db.select({ + imageData: galleryImages.imageData, + mimeType: galleryImages.mimeType, + }).from(galleryImages).where(eq(galleryImages.id, id)).limit(1); + + if (!image || !image.imageData) { + return reply.code(404).send({ error: 'Image not found' }); + } + + const buffer = Buffer.from(image.imageData, 'base64'); + return reply + .header('Content-Type', image.mimeType || 'image/webp') + .header('Cache-Control', 'public, max-age=31536000') + .send(buffer); }); // List all gallery images - admin only fastify.get('/gallery', { preHandler: [fastify.authenticate], - }, async (request, reply) => { - const images = await db.select().from(galleryImages).orderBy(galleryImages.displayOrder); - return { images }; + }, async () => { + const images = await db.select({ + id: galleryImages.id, + altText: galleryImages.altText, + displayOrder: galleryImages.displayOrder, + isPublished: galleryImages.isPublished, + }).from(galleryImages).orderBy(galleryImages.displayOrder); + + return { + images: images.map(img => ({ + ...img, + imageUrl: `/api/gallery/${img.id}/image`, + })) + }; }); - // Get single gallery image + // Get single gallery image metadata fastify.get('/gallery/:id', { preHandler: [fastify.authenticate], }, async (request, reply) => { const { id } = request.params as { id: string }; - const image = await db.select().from(galleryImages).where(eq(galleryImages.id, id)).limit(1); + const [image] = await db.select({ + id: galleryImages.id, + altText: galleryImages.altText, + displayOrder: galleryImages.displayOrder, + isPublished: galleryImages.isPublished, + }).from(galleryImages).where(eq(galleryImages.id, id)).limit(1); - if (image.length === 0) { + if (!image) { return reply.code(404).send({ error: 'Image not found' }); } - return { image: image[0] }; + return { + image: { + ...image, + imageUrl: `/api/gallery/${image.id}/image`, + } + }; }); - // Create gallery image + // Create gallery image (JSON, no file) fastify.post('/gallery', { - schema: { - body: galleryBodyJsonSchema, - }, + schema: { body: galleryBodyJsonSchema }, preHandler: [fastify.authenticate], }, async (request, reply) => { const data = request.body as any; - const [newImage] = await db.insert(galleryImages).values(data).returning(); - - return reply.code(201).send({ image: newImage }); + return reply.code(201).send({ + image: { + ...newImage, + imageUrl: `/api/gallery/${newImage.id}/image`, + } + }); }); // Upload image file (multipart) @@ -69,7 +117,6 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { 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' }); @@ -84,11 +131,6 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { 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', '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) { @@ -96,46 +138,44 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { } 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}`; + // Process image and convert to base64 + let imageData: string; + let mimeType = 'image/webp'; - // 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(); + const processedBuffer = await sharp(inputBuffer) + .rotate() + .resize({ width: 1600, withoutEnlargement: true }) + .webp({ quality: 82 }) + .toBuffer(); + imageData = processedBuffer.toString('base64'); } 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(); + imageData = inputBuffer.toString('base64'); + mimeType = mime; } - const filename = baseName + outExt; - const destPath = path.join(uploadDir, filename); - fs.writeFileSync(destPath, outBuffer); - - // Public URL (served via /static) - const publicUrl = `/images/gallery/${filename}`; - - // Store in DB (optional but useful) + // Store in DB const [row] = await db.insert(galleryImages).values({ - imageUrl: publicUrl, - altText: altText || filename, + imageData, + mimeType, + altText: altText || 'Gallery image', displayOrder, isPublished: true, - }).returning(); + }).returning({ + id: galleryImages.id, + altText: galleryImages.altText, + displayOrder: galleryImages.displayOrder, + isPublished: galleryImages.isPublished, + }); - return reply.code(201).send({ image: row }); + return reply.code(201).send({ + image: { + ...row, + imageUrl: `/api/gallery/${row.id}/image`, + } + }); } catch (err) { fastify.log.error({ err }, 'Upload failed'); @@ -145,25 +185,33 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { // Update gallery image fastify.put('/gallery/:id', { - schema: { - body: galleryBodyJsonSchema, - }, + schema: { body: galleryBodyJsonSchema }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { id } = request.params as { id: string }; const data = request.body as any; const [updated] = await db - .update(galleryImages) - .set(data) - .where(eq(galleryImages.id, id)) - .returning(); + .update(galleryImages) + .set(data) + .where(eq(galleryImages.id, id)) + .returning({ + id: galleryImages.id, + altText: galleryImages.altText, + displayOrder: galleryImages.displayOrder, + isPublished: galleryImages.isPublished, + }); if (!updated) { return reply.code(404).send({ error: 'Image not found' }); } - return { image: updated }; + return { + image: { + ...updated, + imageUrl: `/api/gallery/${updated.id}/image`, + } + }; }); // Delete gallery image @@ -173,9 +221,9 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { const { id } = request.params as { id: string }; const [deleted] = await db - .delete(galleryImages) - .where(eq(galleryImages.id, id)) - .returning(); + .delete(galleryImages) + .where(eq(galleryImages.id, id)) + .returning(); if (!deleted) { return reply.code(404).send({ error: 'Image not found' }); @@ -209,7 +257,6 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { }, async (request, reply) => { const { orders } = request.body as { orders: Array<{ id: string; displayOrder: number }> }; - // Update all in synchronous transaction (better-sqlite3 requirement) db.transaction((tx: any) => { for (const { id, displayOrder } of orders) { tx.update(galleryImages).set({ displayOrder }).where(eq(galleryImages.id, id)).run?.(); @@ -220,4 +267,4 @@ const galleryRoute: FastifyPluginAsync = async (fastify) => { }); }; -export default galleryRoute; +export default galleryRoute; \ No newline at end of file diff --git a/src/pages/admin.astro b/src/pages/admin.astro index 463d3ac..254f3fa 100644 --- a/src/pages/admin.astro +++ b/src/pages/admin.astro @@ -3,370 +3,391 @@ const title = 'Admin'; --- - - - - {title} - - - -

Admin

-
-

Authentifizierung

-
Prüfe Anmeldestatus...
-
- Mit Gitea anmelden - - + + + + {title} + + + +

Admin

+
+

Authentifizierung

+
Prüfe Anmeldestatus...
+
+ Mit Gitea anmelden + + +
+
+ + +
+
+ +
- + - + - + - - + document.getElementById('btn-create-gal').addEventListener('click', async () => { + const file = (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 } + }); + + refreshAuth(); + + + \ No newline at end of file