Compare commits
5 Commits
dev_2
...
89640a3372
| Author | SHA1 | Date | |
|---|---|---|---|
| 89640a3372 | |||
| 4ed0016be9 | |||
| 745888d01b | |||
| febd5a886c | |||
| 25305c4aad |
@ -1,220 +0,0 @@
|
||||
# 🎯 Gallus Pub CMS - System-Erklärung
|
||||
|
||||
## 📐 Architektur-Überblick
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ Frontend │ │ Backend CMS │ │ Fly.io Volume │
|
||||
│ (Astro SSG) │◄────────┤ (Fastify API) │◄────────┤ /app/data/ │
|
||||
│ gallus-pub.ch │ fetch │ cms.gallus-pub.ch│ mount │ - SQLite DB │
|
||||
│ │ │ │ │ - images/ │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
## 🔄 Wie funktioniert der Upload-Flow?
|
||||
|
||||
### 1. **Admin lädt Bild hoch** (admin.astro)
|
||||
```
|
||||
User wählt Bild → uploadImage() → POST /api/gallery/upload
|
||||
```
|
||||
|
||||
### 2. **Backend verarbeitet Upload** (backend/src/routes/gallery.ts)
|
||||
```javascript
|
||||
// Line 60-134 in gallery.ts
|
||||
1. Empfange Multipart-File
|
||||
2. Validiere Mimetype (nur images/*)
|
||||
3. Lese Stream in Buffer
|
||||
4. sharp() konvertiert zu WebP:
|
||||
- Auto-rotate (EXIF)
|
||||
- Resize auf max 1600px
|
||||
- WebP quality 82%
|
||||
5. Speichere in /app/data/images/gallery/
|
||||
6. Erstelle DB-Eintrag mit imageUrl
|
||||
7. Return imageUrl an Frontend
|
||||
```
|
||||
|
||||
### 3. **Bilder werden serviert** (backend/src/index.ts)
|
||||
```javascript
|
||||
// Line 63-69
|
||||
fastifyStatic → /static/ → /app/data/
|
||||
```
|
||||
|
||||
Beispiel:
|
||||
- Bild liegt in: `/app/data/images/gallery/miyma9zc-8he1di.webp`
|
||||
- URL ist: `/images/gallery/miyma9zc-8he1di.webp`
|
||||
- Wird serviert als: `https://cms.gallus-pub.ch/static/images/gallery/miyma9zc-8he1di.webp`
|
||||
|
||||
### 4. **Frontend zeigt Bilder** (src/pages/index.astro)
|
||||
```javascript
|
||||
// Line 30-43
|
||||
1. Fetch von /api/gallery/public
|
||||
2. Map imageUrl: `${API_BASE}${img.imageUrl}`
|
||||
3. Result: https://cms.gallus-pub.ch/static/images/gallery/xyz.webp
|
||||
```
|
||||
|
||||
## 💾 Warum funktioniert SQLite mit Fly.io?
|
||||
|
||||
**Fly.io Volumes** sind persistente Speicher:
|
||||
- Gemountet als: `/app/data/`
|
||||
- Konfiguration in: `backend/fly.toml` (Line 40-42)
|
||||
- Bleibt bei Restarts/Deploys erhalten
|
||||
|
||||
```toml
|
||||
[mounts]
|
||||
source = "gallus_data"
|
||||
destination = "/app/data"
|
||||
```
|
||||
|
||||
### Was liegt wo?
|
||||
|
||||
```
|
||||
/app/data/
|
||||
├── gallus_cms.db # SQLite Datenbank
|
||||
├── gallus_cms.db-wal # Write-Ahead Log
|
||||
├── gallus_cms.db-shm # Shared Memory
|
||||
└── images/
|
||||
├── events/
|
||||
│ ├── event_karaoke.webp
|
||||
│ └── ...
|
||||
└── gallery/
|
||||
├── Gallery1.webp
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 🔒 Datenfluss im Detail
|
||||
|
||||
### Event erstellen:
|
||||
```
|
||||
1. Admin: Bild auswählen + Formular ausfüllen
|
||||
2. uploadImage(file) → /api/gallery/upload
|
||||
↓
|
||||
3. Backend:
|
||||
- Sharp konvertiert → WebP
|
||||
- Speichert in /app/data/images/gallery/
|
||||
- Returnt: { image: { imageUrl: "/images/gallery/xyz.webp" } }
|
||||
↓
|
||||
4. Admin: POST /api/events
|
||||
Body: { title, date, description, imageUrl: "/images/gallery/xyz.webp" }
|
||||
↓
|
||||
5. Backend:
|
||||
- INSERT INTO events (imageUrl = "/images/gallery/xyz.webp")
|
||||
↓
|
||||
6. Frontend: GET /api/events/public
|
||||
- Fetcht Events aus DB
|
||||
- Mapped imageUrl zu voller URL
|
||||
- Zeigt an: <img src="https://cms.gallus-pub.ch/static/images/gallery/xyz.webp">
|
||||
```
|
||||
|
||||
### Warum separate Uploads für Gallery?
|
||||
|
||||
Events nutzen den Gallery-Upload, weil:
|
||||
- Beide brauchen WebP-Konvertierung
|
||||
- Beide nutzen gleichen Storage
|
||||
- Vermeidet Code-Duplikation
|
||||
- Gallery kann eigenständige Bildergalerie haben
|
||||
|
||||
## 🎨 Admin-Panel Features
|
||||
|
||||
### Events-Verwaltung:
|
||||
- ✅ Event erstellen (mit Bild-Upload)
|
||||
- ✅ Events auflisten
|
||||
- ✅ Events löschen
|
||||
- ✅ Reihenfolge ändern (Drag & Drop)
|
||||
- ✅ Events veröffentlichen/verstecken (isPublished)
|
||||
|
||||
### Gallery-Verwaltung: (NEU!)
|
||||
- ✅ Bild hochladen
|
||||
- ✅ Gallery auflisten
|
||||
- ✅ Bilder löschen
|
||||
- ✅ Reihenfolge ändern (Drag & Drop)
|
||||
- ✅ Bilder veröffentlichen/verstecken (isPublished)
|
||||
|
||||
### Publish:
|
||||
- Git-Integration (für statische Seiten-Updates)
|
||||
- Commit & Push zu Repository
|
||||
|
||||
## 🚀 Deployment-Prozess
|
||||
|
||||
### Was passiert beim Deploy?
|
||||
|
||||
1. **Woodpecker CI** triggered bei Push zu `main`
|
||||
2. **Frontend Deploy** (gallus-pub):
|
||||
- Build Astro SSG
|
||||
- Deploy zu Fly.io
|
||||
|
||||
3. **Backend Deploy** (gallus-cms-backend):
|
||||
- Docker Build:
|
||||
```dockerfile
|
||||
# Backend-Code kompilieren
|
||||
npm run build → dist/
|
||||
|
||||
# Migrierte Bilder einpacken
|
||||
COPY backend/data/images → /app/migration-images
|
||||
|
||||
# Migration-Script kopieren
|
||||
COPY migrate-production.js → /app/
|
||||
```
|
||||
- Deploy zu Fly.io
|
||||
- **Volume bleibt erhalten** (SQLite DB + hochgeladene Bilder)
|
||||
|
||||
4. **Nach erstem Deploy**: Migration ausführen
|
||||
```bash
|
||||
fly ssh console -a gallus-cms-backend
|
||||
node migrate-production.js
|
||||
```
|
||||
- Kopiert Bilder: `/app/migration-images/` → `/app/data/images/`
|
||||
- Befüllt DB mit Event/Gallery-Einträgen
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### "Bilder werden nicht angezeigt"
|
||||
|
||||
**Prüfe:**
|
||||
1. Backend logs: `fly logs -a gallus-cms-backend`
|
||||
2. Bild existiert: `fly ssh console -a gallus-cms-backend`
|
||||
```bash
|
||||
ls -la /app/data/images/gallery/
|
||||
```
|
||||
3. DB-Eintrag korrekt:
|
||||
```bash
|
||||
sqlite3 /app/data/gallus_cms.db
|
||||
SELECT * FROM gallery_images;
|
||||
```
|
||||
4. Static-Route funktioniert:
|
||||
```bash
|
||||
curl https://cms.gallus-pub.ch/static/images/gallery/xyz.webp
|
||||
```
|
||||
|
||||
### "SQLite locked" Fehler
|
||||
|
||||
- Nur ein Writer zur Zeit erlaubt
|
||||
- Bei hohem Traffic: Wechsel zu PostgreSQL empfohlen
|
||||
- Für Gallus Pub: ausreichend (wenig Writes)
|
||||
|
||||
### "Volume voll"
|
||||
|
||||
```bash
|
||||
fly volumes list -a gallus-cms-backend
|
||||
fly volumes extend <volume-id> -s <new-size>
|
||||
```
|
||||
|
||||
## 📁 Wichtige Dateien
|
||||
|
||||
| Datei | Zweck |
|
||||
|-------|-------|
|
||||
| `backend/src/routes/gallery.ts` | Gallery-Upload & CRUD |
|
||||
| `backend/src/routes/events.ts` | Events CRUD + Public API |
|
||||
| `backend/src/index.ts` | Static File Serving |
|
||||
| `backend/migrate-production.js` | Initiale Daten-Migration |
|
||||
| `src/pages/admin.astro` | Admin-Interface |
|
||||
| `src/pages/index.astro` | Frontend (fetched von API) |
|
||||
| `backend/fly.toml` | Backend Fly.io Config |
|
||||
| `fly.toml` | Frontend Fly.io Config |
|
||||
|
||||
## 🎯 Nächste Schritte
|
||||
|
||||
1. ✅ Gallery-Verwaltung implementiert
|
||||
2. ⏳ Migration ausführen (nach Deploy)
|
||||
3. ⏳ Testen: Bild hochladen → Frontend anzeigen
|
||||
4. 📋 Optional: Image-Editing Features
|
||||
5. 📋 Optional: Bulk-Upload für Gallery
|
||||
@ -34,6 +34,10 @@ COPY --from=builder /app/node_modules ./node_modules
|
||||
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
|
||||
|
||||
|
||||
183
backend/migrate-production.js
Normal file
183
backend/migrate-production.js
Normal 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);
|
||||
});
|
||||
BIN
images/gallery/miyrhqng-i2kgtx.webp
Normal file
BIN
images/gallery/miyrhqng-i2kgtx.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@ -68,28 +68,6 @@ 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>Liste</h3>
|
||||
<div class="toolbar">
|
||||
<button id="btn-toggle-gal-reorder">Reihenfolge bearbeiten</button>
|
||||
<button id="btn-save-gal-order" style="display:none">Reihenfolge speichern</button>
|
||||
<span id="gal-order-msg" class="muted"></span>
|
||||
</div>
|
||||
<div id="gallery-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>
|
||||
@ -114,11 +92,9 @@ 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-publish').style.display = '';
|
||||
// Direkt Events laden und auf Sektion fokussieren
|
||||
await loadEvents();
|
||||
await loadGallery();
|
||||
document.getElementById('sec-events').scrollIntoView({ behavior: 'smooth' });
|
||||
} catch (e) {
|
||||
const el = document.getElementById('auth-status');
|
||||
@ -312,131 +288,6 @@ const title = 'Admin';
|
||||
} catch(e){ msg.textContent = 'Fehler: '+e.message }
|
||||
});
|
||||
|
||||
// ========== Gallery Management ==========
|
||||
let galReorderMode = false;
|
||||
let lastGallery = [];
|
||||
|
||||
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 = '';
|
||||
lastGallery = (data.images || []).slice();
|
||||
let renderList = lastGallery.slice();
|
||||
|
||||
if (galReorderMode) {
|
||||
renderList.sort((a,b) => (a.displayOrder??0) - (b.displayOrder??0));
|
||||
}
|
||||
|
||||
renderList.forEach((img, idx) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.setAttribute('draggable', String(galReorderMode));
|
||||
card.dataset.id = img.id;
|
||||
card.dataset.displayOrder = String(img.displayOrder ?? idx);
|
||||
card.innerHTML = `
|
||||
<div class="row" style="justify-content:space-between;align-items:center">
|
||||
<div><strong>${img.altText}</strong></div>
|
||||
${galReorderMode ? '<span class="drag-handle">⇅ Ziehen</span>' : ''}
|
||||
</div>
|
||||
<img src="${API_BASE}${img.imageUrl}" alt="${img.altText}" style="max-width:100%;height:auto;margin:0.5rem 0;" />
|
||||
<div class="muted">URL: ${img.imageUrl}</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); }
|
||||
})
|
||||
})
|
||||
|
||||
if (galReorderMode) {
|
||||
enableGalleryDragAndDrop(listEl);
|
||||
}
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '<div class="muted">Fehler beim Laden</div>';
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function enableGalleryDragAndDrop(container){
|
||||
let draggingEl = null;
|
||||
container.querySelectorAll('.card').forEach(card => {
|
||||
card.addEventListener('dragstart', (e) => {
|
||||
draggingEl = card; card.classList.add('dragging');
|
||||
e.dataTransfer.setData('text/plain', card.dataset.id || '');
|
||||
});
|
||||
card.addEventListener('dragend', () => { draggingEl = null; card.classList.remove('dragging'); });
|
||||
card.addEventListener('dragover', (e) => { e.preventDefault(); });
|
||||
card.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
const target = card;
|
||||
if (!draggingEl || draggingEl === target) return;
|
||||
const cards = Array.from(container.querySelectorAll('.card'));
|
||||
const draggingIdx = cards.indexOf(draggingEl);
|
||||
const targetIdx = cards.indexOf(target);
|
||||
if (draggingIdx < targetIdx) {
|
||||
target.after(draggingEl);
|
||||
} else {
|
||||
target.before(draggingEl);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 wähle ein Bild aus';
|
||||
return;
|
||||
}
|
||||
|
||||
msg.textContent = 'Lade Bild hoch...';
|
||||
try {
|
||||
const up = await uploadImage(file, alt || 'Gallery Image');
|
||||
msg.textContent = 'Bild hochgeladen';
|
||||
document.getElementById('gal-file').value = '';
|
||||
document.getElementById('gal-alt').value = '';
|
||||
await loadGallery();
|
||||
} catch(e){
|
||||
msg.textContent = 'Fehler: '+e.message
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btn-toggle-gal-reorder').addEventListener('click', async () => {
|
||||
galReorderMode = !galReorderMode;
|
||||
document.getElementById('btn-save-gal-order').style.display = galReorderMode ? '' : 'none';
|
||||
await loadGallery();
|
||||
});
|
||||
|
||||
document.getElementById('btn-save-gal-order').addEventListener('click', async () => {
|
||||
const container = document.getElementById('gallery-list');
|
||||
const cards = Array.from(container.querySelectorAll('.card'));
|
||||
const orders = cards.map((c, idx) => ({ id: c.dataset.id, displayOrder: idx }));
|
||||
const msg = document.getElementById('gal-order-msg');
|
||||
msg.textContent = 'Speichere Reihenfolge...';
|
||||
try {
|
||||
await api('/api/gallery/reorder', { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ orders }) });
|
||||
msg.textContent = 'Reihenfolge gespeichert';
|
||||
galReorderMode = false;
|
||||
document.getElementById('btn-save-gal-order').style.display = 'none';
|
||||
await loadGallery();
|
||||
} catch(e){ msg.textContent = 'Fehler: '+e.message }
|
||||
});
|
||||
|
||||
refreshAuth();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -8,39 +8,14 @@ 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';
|
||||
const events = [
|
||||
|
||||
// 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);
|
||||
}
|
||||
];
|
||||
|
||||
// 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: "/static/images/gallery/miyma9zc-8he1di.webp", alt: "Schwarzes bild" },
|
||||
{ src: "/static/images/gallery/miyrhqng-i2kgtx.webp", alt: "test" }
|
||||
];
|
||||
---
|
||||
|
||||
<Layout>
|
||||
|
||||
Reference in New Issue
Block a user