Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88ad16c86f | ||
|
|
45ce268db6 | ||
|
|
c1ab18fd9f | ||
|
|
52d6457797 | ||
|
|
4c9be79282 | ||
|
|
324f0f706f | ||
|
|
f7a8c1816f | ||
|
|
d14914a453 | ||
|
|
2a1d56d2fc | ||
|
|
c8ed065486 | ||
|
|
8e6c1924b8 | ||
|
|
04dbd0bb0c | ||
|
|
9b697fbb05 | ||
|
|
48ae3d8166 | ||
|
|
4643ab9b59 | ||
|
|
7f744de577 | ||
|
|
9623abb44a | ||
|
|
d34c55a40b | ||
|
|
9064d58796 | ||
|
|
2ccf195769 | ||
|
|
37495cfca8 | ||
|
|
7097aa7336 | ||
|
|
0b7d3a45b6 | ||
|
|
43f529ad7c | ||
|
|
c026a98d1e | ||
|
|
090da72e6b | ||
|
|
94b1d2c3b4 | ||
|
|
dbe18e6704 | ||
|
|
387ef209ab | ||
|
|
3b27cbd194 | ||
|
|
61842ebc70 | ||
|
|
4e2418116f | ||
|
|
c1fd535549 | ||
|
|
78f5da9cff | ||
|
|
b283816713 | ||
|
|
c77bf3e757 | ||
|
|
36b2053642 | ||
|
|
c3898170fd | ||
|
|
4cc1b21c05 | ||
|
|
e41334a7cc | ||
|
|
47743e9239 | ||
|
|
d271378912 | ||
|
|
4cc6c4f210 | ||
|
|
fde4adfad5 | ||
|
|
3e530e0ac5 | ||
|
|
d8153ed619 | ||
|
|
3df25da009 | ||
|
|
a181993ed5 | ||
|
|
c289541cd5 | ||
|
|
c9d067b1e3 | ||
|
|
4533f6cc3d | ||
|
|
4f12ebaa9a | ||
|
|
a7d53ffe21 | ||
|
|
c723e4919d | ||
|
|
f8cbc60a60 | ||
|
|
feec8ed314 | ||
|
|
2b64a21f16 | ||
|
|
20feee84a6 | ||
|
|
bf7e38ba2d | ||
|
|
d0101b2974 | ||
|
|
75f0c41e5c | ||
|
|
ffadf378f9 | ||
|
|
5e7425cadf | ||
|
|
7a067f60ff | ||
|
|
980200f963 | ||
|
|
de278fab29 | ||
|
|
160d384143 | ||
|
|
f440cbb7f3 | ||
|
|
72faefc88d | ||
|
|
e4ada94390 | ||
|
|
4eab0e6dd2 | ||
|
|
6222d5f19c | ||
|
|
c45e054787 | ||
|
|
8eb2be8628 | ||
|
|
3aafda5f70 | ||
|
|
f27e9a0027 | ||
|
|
921d2527e0 | ||
|
|
901223fcd9 | ||
|
|
357d5ba077 | ||
|
|
5d0c0a0b17 | ||
|
|
cb483d8715 | ||
|
|
3fd5dcf6dd |
@@ -1,4 +1,85 @@
|
||||
steps:
|
||||
audit_dependencies:
|
||||
image: node:20
|
||||
commands:
|
||||
- npm install --package-lock-only
|
||||
- npm audit --audit-level=moderate --json > audit-result.json 2>&1 || echo "Audit completed"
|
||||
- npm audit --audit-level=moderate > audit-output.txt 2>&1 || echo "Audit completed"
|
||||
when:
|
||||
- branch: main
|
||||
event: push
|
||||
|
||||
discord_notify_audit:
|
||||
image: alpine:latest
|
||||
environment:
|
||||
DISCORD_WEBHOOK:
|
||||
from_secret: discord_webhook
|
||||
commands:
|
||||
- apk add --no-cache curl jq
|
||||
- |
|
||||
if [ -f audit-result.json ]; then
|
||||
TOTAL=$(jq -r '.metadata.vulnerabilities.total // 0' audit-result.json 2>/dev/null || echo "0")
|
||||
CRITICAL=$(jq -r '.metadata.vulnerabilities.critical // 0' audit-result.json 2>/dev/null || echo "0")
|
||||
HIGH=$(jq -r '.metadata.vulnerabilities.high // 0' audit-result.json 2>/dev/null || echo "0")
|
||||
MODERATE=$(jq -r '.metadata.vulnerabilities.moderate // 0' audit-result.json 2>/dev/null || echo "0")
|
||||
LOW=$(jq -r '.metadata.vulnerabilities.low // 0' audit-result.json 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ] || [ "$MODERATE" -gt 0 ]; then
|
||||
COLOR=16744448
|
||||
STATUS="⚠️ Vulnerabilities Found"
|
||||
else
|
||||
COLOR=3066993
|
||||
STATUS="✅ No Vulnerabilities"
|
||||
fi
|
||||
|
||||
if [ -f audit-output.txt ]; then
|
||||
VULNS=$(head -50 audit-output.txt | tail -40 || echo "No details")
|
||||
else
|
||||
VULNS="No audit output available"
|
||||
fi
|
||||
|
||||
printf '%s' "$VULNS" > /tmp/vulns.txt
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg title "🔒 Security Audit - Build #${CI_BUILD_NUMBER}" \
|
||||
--arg status "$STATUS" \
|
||||
--arg total "$TOTAL" \
|
||||
--arg critical "$CRITICAL" \
|
||||
--arg high "$HIGH" \
|
||||
--arg moderate "$MODERATE" \
|
||||
--arg low "$LOW" \
|
||||
--arg commit "${CI_COMMIT_SHA:0:7}" \
|
||||
--rawfile details /tmp/vulns.txt \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
||||
--argjson color "$COLOR" \
|
||||
'{
|
||||
embeds: [{
|
||||
title: $title,
|
||||
description: $status,
|
||||
color: $color,
|
||||
fields: [
|
||||
{ name: "Total", value: $total, inline: true },
|
||||
{ name: "Critical", value: $critical, inline: true },
|
||||
{ name: "High", value: $high, inline: true },
|
||||
{ name: "Moderate", value: $moderate, inline: true },
|
||||
{ name: "Low", value: $low, inline: true },
|
||||
{ name: "Commit", value: ("`" + $commit + "`"), inline: true },
|
||||
{ name: "Details", value: ("```\n" + ($details[:800]) + (if ($details | length) > 800 then "\n... (truncated)" else "" end) + "\n```"), inline: false }
|
||||
],
|
||||
timestamp: $timestamp
|
||||
}]
|
||||
}')
|
||||
|
||||
curl -H "Content-Type: application/json" -X POST \
|
||||
-d "$PAYLOAD" "$DISCORD_WEBHOOK"
|
||||
else
|
||||
echo "No audit results found - listing workspace files:"
|
||||
ls -la
|
||||
fi
|
||||
when:
|
||||
- branch: main
|
||||
event: push
|
||||
|
||||
deploy_frontend:
|
||||
image: node:20
|
||||
environment:
|
||||
@@ -9,5 +90,87 @@ steps:
|
||||
- export PATH="$HOME/.fly/bin:$PATH"
|
||||
- flyctl deploy --config fly.toml --app gallus-pub --remote-only
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
- branch: main
|
||||
event: push
|
||||
|
||||
notify_success:
|
||||
image: alpine:latest
|
||||
environment:
|
||||
DISCORD_WEBHOOK:
|
||||
from_secret: discord_webhook
|
||||
commands:
|
||||
- apk add --no-cache curl jq
|
||||
- |
|
||||
# Schreibe Commit-Message in Datei (sicher gegen Shell-Sonderzeichen)
|
||||
printf '%s\n' "$CI_COMMIT_MESSAGE" > /tmp/commit_msg.txt
|
||||
|
||||
PAYLOAD=$(cat /tmp/commit_msg.txt | jq -Rs \
|
||||
--arg title "✅ Build #${CI_BUILD_NUMBER} - Success" \
|
||||
--arg repo "${CI_REPO}" \
|
||||
--arg branch "${CI_COMMIT_BRANCH}" \
|
||||
--arg commit "${CI_COMMIT_SHA:0:7}" \
|
||||
--arg author "${CI_COMMIT_AUTHOR}" \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
||||
'. as $message | {
|
||||
embeds: [{
|
||||
title: $title,
|
||||
description: "Build und Deployment erfolgreich abgeschlossen!",
|
||||
color: 3066993,
|
||||
fields: [
|
||||
{ name: "Repository", value: $repo, inline: true },
|
||||
{ name: "Branch", value: $branch, inline: true },
|
||||
{ name: "Commit", value: ("`" + $commit + "`"), inline: true },
|
||||
{ name: "Author", value: $author, inline: true },
|
||||
{ name: "Commit Message", value: $message, inline: false }
|
||||
],
|
||||
timestamp: $timestamp
|
||||
}]
|
||||
}')
|
||||
|
||||
curl -H "Content-Type: application/json" -X POST \
|
||||
-d "$PAYLOAD" "$DISCORD_WEBHOOK"
|
||||
when:
|
||||
- branch: main
|
||||
event: push
|
||||
status: success
|
||||
|
||||
notify_failure:
|
||||
image: alpine:latest
|
||||
environment:
|
||||
DISCORD_WEBHOOK:
|
||||
from_secret: discord_webhook
|
||||
commands:
|
||||
- apk add --no-cache curl jq
|
||||
- |
|
||||
# Schreibe Commit-Message in Datei (sicher gegen Shell-Sonderzeichen)
|
||||
printf '%s\n' "$CI_COMMIT_MESSAGE" > /tmp/commit_msg.txt
|
||||
|
||||
PAYLOAD=$(cat /tmp/commit_msg.txt | jq -Rs \
|
||||
--arg title "❌ Build #${CI_BUILD_NUMBER} - Failure" \
|
||||
--arg repo "${CI_REPO}" \
|
||||
--arg branch "${CI_COMMIT_BRANCH}" \
|
||||
--arg commit "${CI_COMMIT_SHA:0:7}" \
|
||||
--arg author "${CI_COMMIT_AUTHOR}" \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
||||
'. as $message | {
|
||||
embeds: [{
|
||||
title: $title,
|
||||
description: "Build oder Deployment ist fehlgeschlagen!",
|
||||
color: 15158332,
|
||||
fields: [
|
||||
{ name: "Repository", value: $repo, inline: true },
|
||||
{ name: "Branch", value: $branch, inline: true },
|
||||
{ name: "Commit", value: ("`" + $commit + "`"), inline: true },
|
||||
{ name: "Author", value: $author, inline: true },
|
||||
{ name: "Commit Message", value: $message, inline: false }
|
||||
],
|
||||
timestamp: $timestamp
|
||||
}]
|
||||
}')
|
||||
|
||||
curl -H "Content-Type: application/json" -X POST \
|
||||
-d "$PAYLOAD" "$DISCORD_WEBHOOK"
|
||||
when:
|
||||
- branch: main
|
||||
event: push
|
||||
status: failure
|
||||
@@ -39,8 +39,9 @@ RUN npm ci --omit=dev || npm install --production && \
|
||||
RUN apk del python3 make g++ vips-dev
|
||||
|
||||
# Copy built files from builder
|
||||
# Hinweis: Drizzle-Migrationen werden nicht kopiert - das Schema wird zur
|
||||
# Laufzeit von initDatabase() angelegt, src/db/migrations existiert nicht.
|
||||
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
|
||||
|
||||
@@ -14,7 +14,7 @@ primary_region = "ams"
|
||||
GIT_WORKSPACE_DIR = "/app/data/workspace"
|
||||
# Cross-site frontend and OAuth
|
||||
FRONTEND_URL = "https://gallus-pub.ch"
|
||||
CORS_ORIGIN = "https://gallus-pub.ch"
|
||||
CORS_ORIGIN = "https://gallus-pub.ch,https://www.gallus-pub.ch"
|
||||
GITEA_REDIRECT_URI = "https://cms.gallus-pub.ch/api/auth/callback"
|
||||
|
||||
[http_service]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
// Users table - stores Gitea user info for audit and access control
|
||||
export const users = sqliteTable('users', {
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
giteaId: text('gitea_id').notNull().unique(),
|
||||
@@ -60,3 +59,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())`),
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
@@ -39,8 +40,24 @@ const fastify = Fastify({
|
||||
});
|
||||
|
||||
// Register plugins
|
||||
// Support multiple origins for CORS
|
||||
const allowedOrigins = env.CORS_ORIGIN.split(',').map(o => o.trim());
|
||||
fastify.register(cors, {
|
||||
origin: env.CORS_ORIGIN,
|
||||
origin: (origin, cb) => {
|
||||
// Allow requests with no origin (like mobile apps or curl)
|
||||
if (!origin) {
|
||||
return cb(null, true);
|
||||
}
|
||||
|
||||
// Check if origin is in allowed list
|
||||
const isAllowed = allowedOrigins.includes(origin);
|
||||
|
||||
if (isAllowed) {
|
||||
return cb(null, true);
|
||||
} else {
|
||||
return cb(null, false);
|
||||
}
|
||||
},
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
@@ -78,6 +95,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 () => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { eq } from 'drizzle-orm';
|
||||
import { GiteaService } from '../services/gitea.service.js';
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
// Use explicit JSON schema for Fastify route validation to avoid provider issues
|
||||
const callbackQueryJsonSchema = {
|
||||
type: 'object',
|
||||
required: ['code', 'state'],
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { db } from '../config/database.js';
|
||||
import { banners } from '../db/schema.js';
|
||||
import { eq, and, lte, gte, desc } from 'drizzle-orm';
|
||||
|
||||
const bannerBodyJsonSchema = {
|
||||
type: 'object',
|
||||
required: ['text', 'startDate', 'endDate'],
|
||||
properties: {
|
||||
text: { type: 'string' },
|
||||
startDate: { type: 'string' },
|
||||
endDate: { type: 'string' },
|
||||
isActive: { type: 'boolean' },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const bannersRoute: FastifyPluginAsync = async (fastify) => {
|
||||
|
||||
// Get active banner (public endpoint)
|
||||
fastify.get('/banners/active', async (request, reply) => {
|
||||
// Use local date to avoid timezone issues
|
||||
const now = new Date();
|
||||
const today = new Date(now.getTime() - (now.getTimezoneOffset() * 60000))
|
||||
.toISOString()
|
||||
.split('T')[0]; // YYYY-MM-DD
|
||||
|
||||
const [activeBanner] = await db
|
||||
.select()
|
||||
.from(banners)
|
||||
.where(
|
||||
and(
|
||||
eq(banners.isActive, true),
|
||||
lte(banners.startDate, today),
|
||||
gte(banners.endDate, today)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(banners.createdAt))
|
||||
.limit(1);
|
||||
|
||||
if (!activeBanner) {
|
||||
return { banner: null };
|
||||
}
|
||||
|
||||
return {
|
||||
banner: {
|
||||
id: activeBanner.id,
|
||||
text: activeBanner.text,
|
||||
startDate: activeBanner.startDate,
|
||||
endDate: activeBanner.endDate,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Get all banners (admin only)
|
||||
fastify.get('/banners', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const allBanners = await db.select().from(banners);
|
||||
|
||||
return {
|
||||
banners: allBanners.map((b: any) => ({
|
||||
id: b.id,
|
||||
text: b.text,
|
||||
startDate: b.startDate,
|
||||
endDate: b.endDate,
|
||||
isActive: b.isActive,
|
||||
createdAt: b.createdAt,
|
||||
updatedAt: b.updatedAt,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Create banner (admin only)
|
||||
fastify.post('/banners', {
|
||||
schema: {
|
||||
body: bannerBodyJsonSchema,
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { text, startDate, endDate, isActive = true } = request.body as any;
|
||||
|
||||
const [newBanner] = await db
|
||||
.insert(banners)
|
||||
.values({
|
||||
text,
|
||||
startDate,
|
||||
endDate,
|
||||
isActive,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return {
|
||||
banner: {
|
||||
id: newBanner.id,
|
||||
text: newBanner.text,
|
||||
startDate: newBanner.startDate,
|
||||
endDate: newBanner.endDate,
|
||||
isActive: newBanner.isActive,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Update banner (admin only)
|
||||
fastify.put('/banners/:id', {
|
||||
schema: {
|
||||
body: bannerBodyJsonSchema,
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const { text, startDate, endDate, isActive } = request.body as any;
|
||||
|
||||
const [updated] = await db
|
||||
.update(banners)
|
||||
.set({
|
||||
text,
|
||||
startDate,
|
||||
endDate,
|
||||
isActive,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(banners.id, id))
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
return reply.code(404).send({ error: 'Banner not found' });
|
||||
}
|
||||
|
||||
return {
|
||||
banner: {
|
||||
id: updated.id,
|
||||
text: updated.text,
|
||||
startDate: updated.startDate,
|
||||
endDate: updated.endDate,
|
||||
isActive: updated.isActive,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Delete banner (admin only)
|
||||
fastify.delete('/banners/:id', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
|
||||
await db.delete(banners).where(eq(banners.id, id));
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
};
|
||||
|
||||
export default bannersRoute;
|
||||
@@ -94,7 +94,7 @@ const eventsRoute: 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', 'events');
|
||||
const uploadDir = path.join(dataDir, 'public', 'images', 'events');
|
||||
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
||||
|
||||
// Read uploaded stream into buffer
|
||||
@@ -133,7 +133,7 @@ const eventsRoute: FastifyPluginAsync = async (fastify) => {
|
||||
fs.writeFileSync(destPath, outBuffer);
|
||||
|
||||
// Public URL (served via /static)
|
||||
const publicUrl = `/static/images/events/${filename}`;
|
||||
const publicUrl = `/images/events/${filename}`;
|
||||
|
||||
return reply.code(201).send({ imageUrl: publicUrl });
|
||||
|
||||
|
||||
@@ -86,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
|
||||
@@ -125,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({
|
||||
|
||||
@@ -43,6 +43,7 @@ export class FileGeneratorService {
|
||||
|
||||
return `---
|
||||
import Layout from "../components/Layout.astro";
|
||||
import Banner from "../components/Banner.astro";
|
||||
import Hero from "../components/Hero.astro";
|
||||
import Welcome from "../components/Welcome.astro";
|
||||
import EventsGrid from "../components/EventsGrid.astro";
|
||||
@@ -62,6 +63,7 @@ ${imagesCode}
|
||||
|
||||
<Layout>
|
||||
\t<Hero id="hero" />
|
||||
\t<Banner />
|
||||
\t<Welcome id="welcome" />
|
||||
\t<EventsGrid id="events" events={events} />
|
||||
\t<ImageCarousel id="gallery" images={images} />
|
||||
|
||||
@@ -1,43 +1,84 @@
|
||||
import simpleGit, { SimpleGit } from 'simple-git';
|
||||
import { mkdir, rm } from 'fs/promises';
|
||||
import { mkdir, rm, cp } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
export class GitService {
|
||||
private git: SimpleGit;
|
||||
private workspaceDir: string;
|
||||
private parentDir: string;
|
||||
private repoUrl: string;
|
||||
private token: string;
|
||||
|
||||
constructor() {
|
||||
this.workspaceDir = env.GIT_WORKSPACE_DIR;
|
||||
this.parentDir = path.dirname(this.workspaceDir);
|
||||
this.repoUrl = env.GIT_REPO_URL;
|
||||
this.token = env.GIT_TOKEN;
|
||||
this.git = simpleGit();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Ensure workspace directory exists
|
||||
await mkdir(this.workspaceDir, { recursive: true });
|
||||
// Elternverzeichnis muss existieren - simple-git startet git von dort aus
|
||||
await mkdir(this.parentDir, { recursive: true });
|
||||
|
||||
// Add token to repo URL for authentication
|
||||
const authenticatedUrl = this.repoUrl.replace(
|
||||
'https://',
|
||||
`https://oauth2:${this.token}@`
|
||||
`https://oauth2:${encodeURIComponent(this.token)}@`
|
||||
);
|
||||
|
||||
try {
|
||||
// Check if repo already exists
|
||||
await this.git.cwd(this.workspaceDir);
|
||||
await this.git.status();
|
||||
console.log('Repository already exists, pulling latest...');
|
||||
await this.git.pull();
|
||||
} catch {
|
||||
// Clone if doesn't exist
|
||||
let usable = false;
|
||||
|
||||
if (existsSync(path.join(this.workspaceDir, '.git'))) {
|
||||
try {
|
||||
this.git = simpleGit(this.workspaceDir);
|
||||
await this.git.status();
|
||||
console.log('Repository already exists, pulling latest...');
|
||||
await this.git.pull();
|
||||
usable = true;
|
||||
} catch (error) {
|
||||
// Token aus der Meldung entfernen - git gibt die Remote-URL mit aus
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
console.warn(
|
||||
'Existing workspace unusable, re-cloning:',
|
||||
msg.replace(/\/\/[^@\s/]*@/g, '//***@')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!usable) {
|
||||
console.log('Cloning repository...');
|
||||
|
||||
// Hochgeladene Bilder liegen im Workspace und wuerden beim Loeschen
|
||||
// verschwinden - vorher wegsichern, nach dem Clone zurueckspielen
|
||||
const imagesDir = path.join(this.workspaceDir, 'public', 'images');
|
||||
const backupDir = path.join(this.parentDir, '.workspace-images-backup');
|
||||
const hasImages = existsSync(imagesDir);
|
||||
|
||||
await rm(backupDir, { recursive: true, force: true });
|
||||
if (hasImages) {
|
||||
await cp(imagesDir, backupDir, { recursive: true });
|
||||
}
|
||||
|
||||
await rm(this.workspaceDir, { recursive: true, force: true });
|
||||
|
||||
// Aus dem existierenden Elternverzeichnis klonen, nicht aus dem
|
||||
// soeben geloeschten Zielverzeichnis (sonst: spawn git ENOENT)
|
||||
this.git = simpleGit(this.parentDir);
|
||||
await this.git.clone(authenticatedUrl, this.workspaceDir);
|
||||
await this.git.cwd(this.workspaceDir);
|
||||
this.git = simpleGit(this.workspaceDir);
|
||||
|
||||
if (hasImages) {
|
||||
// force: false -> was schon im Repo liegt, bleibt unangetastet
|
||||
await cp(backupDir, imagesDir, {
|
||||
recursive: true,
|
||||
force: false,
|
||||
errorOnExist: false,
|
||||
});
|
||||
await rm(backupDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Configure git user
|
||||
@@ -47,6 +88,14 @@ export class GitService {
|
||||
|
||||
async commitAndPush(message: string): Promise<string> {
|
||||
await this.git.add('.');
|
||||
|
||||
const status = await this.git.status();
|
||||
if (status.isClean()) {
|
||||
// Nichts zu committen - aktuellen Stand zurueckgeben statt zu scheitern
|
||||
const current = await this.git.log({ maxCount: 1 });
|
||||
return current.latest?.hash || '';
|
||||
}
|
||||
|
||||
await this.git.commit(message);
|
||||
await this.git.push('origin', 'main');
|
||||
|
||||
@@ -59,7 +108,15 @@ export class GitService {
|
||||
}
|
||||
|
||||
async reset() {
|
||||
await this.git.reset(['--hard', 'HEAD']);
|
||||
await this.git.clean('f', ['-d']);
|
||||
// Nur zuruecksetzen wenn wirklich ein Repo da ist - sonst laeuft git
|
||||
// im Prozess-Arbeitsverzeichnis und raeumt dort auf
|
||||
if (!existsSync(path.join(this.workspaceDir, '.git'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const git = simpleGit(this.workspaceDir);
|
||||
await git.reset(['--hard', 'HEAD']);
|
||||
// Uploads ausnehmen - die sind noch nicht committed und waeren sonst weg
|
||||
await git.clean('f', ['-d', '-e', 'public/images']);
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 800 KiB |
|
Before Width: | Height: | Size: 747 KiB |
|
Before Width: | Height: | Size: 800 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 469 KiB |
|
Before Width: | Height: | Size: 523 KiB After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 604 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 567 KiB After Width: | Height: | Size: 567 KiB |
|
After Width: | Height: | Size: 469 KiB |
|
After Width: | Height: | Size: 728 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 1021 KiB |
|
After Width: | Height: | Size: 1021 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 488 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" zoomAndPan="magnify" viewBox="0 0 375 374.999991" height="500" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><clipPath id="f68ef4028f"><path d="M 47.203125 40 L 322 40 L 322 335 L 47.203125 335 Z M 47.203125 40 " clip-rule="nonzero"/></clipPath></defs><g clip-path="url(#f68ef4028f)"><path fill="#1d6b32" d="M 48.097656 139.574219 C 48.097656 128.285156 48.660156 119.429688 55.417969 109.972656 C 60.960938 102.21875 68.625 96.976562 75.371094 90.4375 C 90.03125 76.242188 102.058594 52.597656 122.386719 45.964844 C 140.441406 40.078125 161.71875 50.683594 170.175781 67.148438 C 174.488281 75.5625 175.570312 84.835938 176.609375 94.097656 C 178.023438 106.769531 179.671875 119.417969 181.199219 132.078125 C 186.097656 127.304688 184.324219 113.089844 184.800781 106.679688 C 185.414062 98.355469 185.589844 89.773438 187.167969 81.554688 C 190.136719 66.023438 202.230469 53.867188 215.535156 46.148438 C 230.378906 37.53125 245.867188 37.894531 259.476562 48.902344 C 265.457031 53.734375 269.027344 59.921875 273.558594 65.980469 C 278.105469 72.058594 283.742188 77.164062 289.878906 81.59375 C 301.03125 89.632812 313.222656 97.105469 314.707031 111.992188 C 315.859375 123.574219 312.664062 136.851562 306.261719 146.585938 C 298.824219 157.886719 283.566406 165.484375 271.140625 169.84375 C 263.703125 172.453125 256.171875 173.589844 248.375 174.402344 C 243.988281 174.855469 239.605469 175.347656 235.230469 175.863281 C 233.320312 176.085938 220.464844 177.1875 222.21875 178.914062 C 223.414062 180.097656 232.449219 178.90625 234.25 178.929688 C 238.816406 178.984375 243.378906 179.058594 247.9375 179.101562 C 258.195312 179.199219 268.472656 179.238281 278.703125 179.984375 C 294.078125 181.101562 308.125 186.960938 316.167969 200.828125 C 324.128906 214.566406 322.324219 235.332031 313.648438 248.554688 C 309.550781 254.800781 302.960938 258.933594 298.382812 264.773438 C 292.863281 271.808594 289.753906 280.417969 284.703125 287.777344 C 277.070312 298.902344 265.578125 308.597656 251.328125 307.664062 C 237.253906 306.738281 223.84375 295.984375 214.992188 285.644531 C 204.679688 273.609375 202.425781 260.40625 198.433594 245.617188 C 197.992188 243.980469 196.5 234.824219 195.105469 234.082031 C 192.15625 232.503906 193.15625 242.25 193.140625 243.652344 C 193.039062 252.289062 193.183594 260.898438 194.160156 269.492188 C 195.984375 285.542969 200.703125 301.367188 211.675781 313.625 C 215.640625 318.050781 226.738281 326.539062 219.757812 333.71875 C 216.042969 337.535156 203.546875 329.980469 199.726562 326.988281 C 169.355469 303.21875 175.738281 257.648438 175.738281 223.953125 C 158.667969 247.144531 172.769531 289.511719 141.984375 304.472656 C 130.417969 310.089844 113.792969 310.679688 102.148438 305.070312 C 88.4375 298.476562 81.277344 282.652344 73.429688 270.453125 C 69.234375 263.933594 63.84375 258.296875 58.707031 252.453125 C 55.953125 249.324219 52.542969 246.261719 50.21875 242.816406 C 47.457031 238.734375 47.503906 233.292969 48.097656 228.109375 C 49.5 215.78125 54.359375 203.136719 63.367188 194.40625 C 72.792969 185.277344 84.792969 181.804688 97.621094 181.160156 C 106.09375 180.734375 114.582031 180.5625 123.0625 180.191406 C 127.40625 180.003906 131.675781 179.730469 135.988281 179.113281 C 141.285156 178.359375 145.921875 177.105469 138.335938 175.230469 C 109.652344 168.167969 59.332031 176.03125 48.097656 139.574219 C 48.097656 135.066406 52.933594 155.265625 48.097656 139.574219 " fill-opacity="1" fill-rule="nonzero"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,40 @@
|
||||
---
|
||||
// src/components/Banner.astro
|
||||
import "../styles/components/Banner.css"
|
||||
---
|
||||
|
||||
<div id="banner-container"></div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'https://cms.gallus-pub.ch';
|
||||
|
||||
async function loadBanner() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/banners/active`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.banner) {
|
||||
const container = document.getElementById('banner-container');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="banner-wrapper">
|
||||
<div class="banner container">
|
||||
<p>${data.banner.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch banner:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load banner when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadBanner);
|
||||
} else {
|
||||
loadBanner();
|
||||
}
|
||||
</script>
|
||||
@@ -7,8 +7,7 @@ const { id } = Astro.props;
|
||||
<h2 class="title">Drinks</h2>
|
||||
|
||||
<p class="note">
|
||||
Ob ein frisch gezapftes Pint, ein edler Tropfen Whiskey oder ein gemütliches Glas Wein – hier kannst du in entspannter
|
||||
Atmosphäre das Leben genießen. Natürlich dürfen auch Cocktails nicht fehlen. Vieles kreieren wir auch selber - Sláinte!
|
||||
Ob frisch gezapftes Pint, edler Whisky oder ein gemütliches Glas Wein – bei uns genießt du das Leben in entspannter Atmosphäre. Auch Cocktails dürfen nicht fehlen: Vieles kreieren wir selbst. Sláinte!
|
||||
</p>
|
||||
|
||||
<a href="/pdf/Getraenke_Gallus_2025.pdf" class="card-link" target="_blank" rel="noopener noreferrer">Getränkekarte</a>
|
||||
|
||||
@@ -15,6 +15,7 @@ import "../styles/index.css"
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<title>Gallus Pub</title>
|
||||
</head>
|
||||
|
||||
|
||||
@@ -13,41 +13,32 @@ const { id } = Astro.props;
|
||||
<h2>Gallus Pub!</h2>
|
||||
|
||||
<p>
|
||||
Wie die meisten bereits wissen, ist hier jeder willkommen - ob jung
|
||||
oder alt, Rocker, Elf, Nerd, Meerjungfrau oder einfach nur du
|
||||
selbst. Unsere Türen stehen offen für alle, die Spass haben wollen
|
||||
und gute Gesellschaft suchen!
|
||||
Bei uns ist jede*r willkommen – ob jung oder alt, Rocker, Nerd, Meerjungfrau oder einfach du selbst. Unsere Türen stehen allen offen, die Spass und gute Gesellschaft suchen.
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p><b>Unsere Highlights:</b></p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>Karaoke:</b> Von Mittwoch bis Samstag kannst du deine
|
||||
Stimme auf zwei Stockwerken zum Besten geben. Keine Sorge, es geht
|
||||
nicht darum, perfekt zu sein, sondern einfach Spass zu haben! Du singst
|
||||
gerne, aber lieber für dich? Dann kannst du den 2. OG auch privat
|
||||
mieten.
|
||||
<b>Karaoke:</b> Von Mittwoch bis Samstag kannst du auf zwei Stockwerken deine Stimme zum Besten geben – ganz ohne Perfektionsdruck, Hauptsache Spass. Du singst lieber im kleinen Rahmen?
|
||||
Dann kannst du den Sangallerruum im 2.OG auch privat mieten.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>Pub Quiz:</b> Jeden Freitag ab 20:00 Uhr testet ihr
|
||||
euer Wissen in verschiedenen Runden. Jede Woche gibt es ein neues
|
||||
Thema und einen neuen Champion.
|
||||
<b>Pub Quiz:</b> Jeden Freitag ab 20:00 Uhr testet ihr euer Wissen in mehreren Runden. Jede Woche warten ein neues Thema und ein neuer Sieger der Herzen.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>Getränke:</b> Geniesst frisches Guinness, Smithwicks,
|
||||
Gallus Old Style Ale und unsere leckeren Cocktails. Für Whisky-Liebhaber
|
||||
haben wir erlesene Sorten aus Schottland und Irland im Angebot.
|
||||
<b>Getränke:</b> Geniesst frisches Guinness, Smithwick’s, Gallus Old Style Ale, leckere Cocktails und ausgewählte Whiskys aus Schottland und Irland.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Wir freuen uns darauf, euch bald bei uns begrüssen zu können! Lasst
|
||||
uns gemeinsam unvergessliche Abende feiern. - Sabrina & Raphael
|
||||
Wir freuen uns, euch bald bei uns zu begrüssen – auf viele unvergessliche Abende!
|
||||
</p>
|
||||
<p>
|
||||
Sabrina & Raphael
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
import Layout from "../components/Layout.astro";
|
||||
import Banner from "../components/Banner.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Banner />
|
||||
|
||||
<h1>Gallery</h1>
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
import Layout from "../components/Layout.astro";
|
||||
import Banner from "../components/Banner.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Banner />
|
||||
|
||||
<h1>Openings</h1>
|
||||
|
||||
|
||||
@@ -84,6 +84,24 @@ const title = 'Admin';
|
||||
</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>
|
||||
@@ -109,10 +127,12 @@ const title = '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');
|
||||
@@ -120,6 +140,7 @@ const title = 'Admin';
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
@@ -202,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>`;
|
||||
@@ -366,6 +387,94 @@ const title = 'Admin';
|
||||
} 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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import Layout from "../components/Layout.astro";
|
||||
import Banner from "../components/Banner.astro";
|
||||
import Hero from "../components/Hero.astro";
|
||||
import Welcome from "../components/Welcome.astro";
|
||||
import EventsGrid from "../components/EventsGrid.astro";
|
||||
@@ -9,16 +10,68 @@ import Contact from "../components/Contact.astro";
|
||||
import About from "../components/About.astro";
|
||||
|
||||
const events = [
|
||||
|
||||
{
|
||||
image: "/images/events/mmnm5yob-w9xjor.jpeg",
|
||||
title: "Karaoke",
|
||||
date: "2026-03-12",
|
||||
description: `
|
||||
Du singst gerne, aber lieber für dich? Dann kannst du den Sangallerruum im 2OG auch privat mieten. (Optimal für 10-20Pers). Mehr Info's🍀via 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/mr82c7eh-yp87hy.jpeg",
|
||||
title: "Richie Live",
|
||||
date: "2026-08-27",
|
||||
description: `
|
||||
SOLD OUT! - Richie hat das Image eines Weltenbummlers und Musikkünstlers. In der Ostschweiz ist er bereits bestens bekannt.🍀Warteliste: WA 077 232 27 70
|
||||
`,
|
||||
},
|
||||
{
|
||||
image: "/images/events/mrnpux59-b3075p.jpeg",
|
||||
title: "Hannes LIVE",
|
||||
date: "2026-09-10",
|
||||
description: `
|
||||
Er ist zurück!🥰🍀Johannes ist wieder bei uns! Eine weitere unvergessliche Irish-Night!!🍀Sei dabei! Reservationen🍀via WA 077 232 27 70
|
||||
`,
|
||||
},
|
||||
{
|
||||
image: "/images/events/mrngoyyy-49rz7u.jpeg",
|
||||
title: "Danielle&Nina",
|
||||
date: "2026-09-24",
|
||||
description: `
|
||||
SOLD OUT! - First time on stage – und das direkt im Gallus Pub!🎤✨
|
||||
Danielle & Nina bringen Irish Folk & liebevolle Cover Songs.🍀Warteliste: WA 077 232 27 70
|
||||
`,
|
||||
}
|
||||
];
|
||||
|
||||
const images = [
|
||||
{ src: "/static/images/gallery/miyvtrjn-zoq4j5.webp", alt: "miyvtrjn-zoq4j5.webp" }
|
||||
{ src: "/images/gallery/mndhrrtl-s4o8dn.png", alt: "mndhrrtl-s4o8dn.png" },
|
||||
{ src: "/images/gallery/mndhtjvz-965yra.jpeg", alt: "mndhtjvz-965yra.jpeg" },
|
||||
{ src: "/images/gallery/mndi3625-lnnggc.jpeg", alt: "mndi3625-lnnggc.jpeg" },
|
||||
{ src: "/images/gallery/mndi7upa-qxukqg.jpeg", alt: "mndi7upa-qxukqg.jpeg" },
|
||||
{ src: "/images/gallery/mndi8kzg-oazjp8.jpeg", alt: "mndi8kzg-oazjp8.jpeg" },
|
||||
{ src: "/images/gallery/mndiabuo-m29xj0.jpeg", alt: "mndiabuo-m29xj0.jpeg" },
|
||||
{ src: "/images/gallery/mndicr59-n8suoy.jpeg", alt: "mndicr59-n8suoy.jpeg" },
|
||||
{ src: "/images/gallery/mndieqns-mjs7xe.jpeg", alt: "mndieqns-mjs7xe.jpeg" },
|
||||
{ src: "/images/gallery/mndif8nv-aptjf6.jpeg", alt: "mndif8nv-aptjf6.jpeg" },
|
||||
{ src: "/images/gallery/mndiggta-hnldkv.jpeg", alt: "mndiggta-hnldkv.jpeg" },
|
||||
{ src: "/images/gallery/mndih8lp-woprt4.webp", alt: "mndih8lp-woprt4.webp" }
|
||||
];
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Hero id="hero" />
|
||||
<Banner />
|
||||
<Welcome id="welcome" />
|
||||
<EventsGrid id="events" events={events} />
|
||||
<ImageCarousel id="gallery" images={images} />
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
.banner-wrapper {
|
||||
width: 100%;
|
||||
background-color: var(--color-orange1);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.banner {
|
||||
max-width: var(--container-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--padding-horizontal);
|
||||
}
|
||||
|
||||
.banner p {
|
||||
color: #000;
|
||||
font-size: var(--font-size-small-medium);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.banner p {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
}
|
||||