From fa34a6c9fbf93d0151efdb43c7dcf17643153836 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Tue, 18 Aug 2026 22:07:31 +0200 Subject: [PATCH] Add posterImageUrl to events Concert posters already live on the public website, so events store a URL to an existing image rather than hosting an uploaded file. --- sql/feedback/002_add_poster_image_url.sql | 9 +++++++++ src/models/feedback/admin/admin.interface.ts | 6 ++++++ src/models/feedback/admin/events.admin.router.ts | 6 ++++-- src/models/feedback/admin/events.admin.service.ts | 8 +++++--- src/models/feedback/feedback.interface.ts | 5 +++++ src/models/feedback/public/events.public.service.ts | 8 +++++--- 6 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 sql/feedback/002_add_poster_image_url.sql diff --git a/sql/feedback/002_add_poster_image_url.sql b/sql/feedback/002_add_poster_image_url.sql new file mode 100644 index 0000000..c540010 --- /dev/null +++ b/sql/feedback/002_add_poster_image_url.sql @@ -0,0 +1,9 @@ +-- Nachklang e.V. Feedback module — adds a per-event poster image URL. +-- Apply manually against the FEEDBACK_DB database, after 001_init.sql: +-- mysql -h -u -p < 002_add_poster_image_url.sql +-- +-- Stores a URL only (e.g. an existing nachklang.art poster image) rather +-- than an uploaded file — the concert posters already live on the public +-- website, so there is no need for the feedback app to host its own copy. +ALTER TABLE events + ADD COLUMN poster_image_url VARCHAR(500) NULL AFTER intro_text; diff --git a/src/models/feedback/admin/admin.interface.ts b/src/models/feedback/admin/admin.interface.ts index 8326f5c..5b68072 100644 --- a/src/models/feedback/admin/admin.interface.ts +++ b/src/models/feedback/admin/admin.interface.ts @@ -33,6 +33,9 @@ * feedbackDeadline: * type: string * format: date-time + * posterImageUrl: + * type: string + * nullable: true * isPublished: * type: boolean * submissionCount: @@ -87,6 +90,7 @@ export interface EventAdminSummary { subtitle: string | null; eventDate: string; feedbackDeadline: string; + posterImageUrl: string | null; isPublished: boolean; submissionCount: number; } @@ -110,6 +114,7 @@ export interface CreateEventInput { eventDate: string; feedbackDeadline?: string; introText?: string; + posterImageUrl?: string; } export interface UpdateEventInput { @@ -119,6 +124,7 @@ export interface UpdateEventInput { feedbackDeadline?: string; isPublished?: boolean; introText?: string; + posterImageUrl?: string; } export interface AdminQuestion { diff --git a/src/models/feedback/admin/events.admin.router.ts b/src/models/feedback/admin/events.admin.router.ts index 8bcaced..f9b5cda 100644 --- a/src/models/feedback/admin/events.admin.router.ts +++ b/src/models/feedback/admin/events.admin.router.ts @@ -59,6 +59,8 @@ export const eventsAdminRouter = express.Router(); * format: date-time * introText: * type: string + * posterImageUrl: + * type: string * responses: * 201: * description: Created @@ -77,13 +79,13 @@ eventsAdminRouter.get('/', async (req: Request, res: Response) => { eventsAdminRouter.post('/', async (req: Request, res: Response) => { try { - const {name, subtitle, eventDate, feedbackDeadline, introText} = req.body || {}; + const {name, subtitle, eventDate, feedbackDeadline, introText, posterImageUrl} = req.body || {}; if (!name || !eventDate) { res.status(400).send({status: 'BAD_REQUEST', message: 'name and eventDate are required'}); return; } const eventId = await EventsAdminService.createEvent( - {name, subtitle, eventDate, feedbackDeadline, introText}, + {name, subtitle, eventDate, feedbackDeadline, introText, posterImageUrl}, res.locals.admin.email ); res.status(201).send({eventId}); diff --git a/src/models/feedback/admin/events.admin.service.ts b/src/models/feedback/admin/events.admin.service.ts index 70075b2..de3be77 100644 --- a/src/models/feedback/admin/events.admin.service.ts +++ b/src/models/feedback/admin/events.admin.service.ts @@ -38,6 +38,7 @@ const mapSummaryRow = (row: any): EventAdminSummary => ({ subtitle: row.subtitle, eventDate: row.event_date, feedbackDeadline: row.feedback_deadline, + posterImageUrl: row.poster_image_url, isPublished: !!row.is_published, submissionCount: Number(row.submission_count) }); @@ -94,11 +95,11 @@ export const createEvent = async (input: CreateEventInput, createdByEmail: strin : computeDefaultDeadline(input.eventDate); const query = ` - INSERT INTO events (slug, name, subtitle, event_date, feedback_deadline, intro_text, created_by_email) - VALUES (?,?,?,?,?,?,?) RETURNING event_id`; + INSERT INTO events (slug, name, subtitle, event_date, feedback_deadline, intro_text, poster_image_url, created_by_email) + VALUES (?,?,?,?,?,?,?,?) RETURNING event_id`; const res = await conn.query(query, [ slug, input.name, input.subtitle || null, input.eventDate, formatDatetime(deadline), - input.introText || null, createdByEmail + input.introText || null, input.posterImageUrl || null, createdByEmail ]); await conn.commit(); return res[0].event_id; @@ -154,6 +155,7 @@ export const updateEvent = async (eventId: number, input: UpdateEventInput): Pro if (input.feedbackDeadline !== undefined) { fields.push('feedback_deadline = ?'); values.push(input.feedbackDeadline); } if (input.isPublished !== undefined) { fields.push('is_published = ?'); values.push(input.isPublished ? 1 : 0); } if (input.introText !== undefined) { fields.push('intro_text = ?'); values.push(input.introText); } + if (input.posterImageUrl !== undefined) { fields.push('poster_image_url = ?'); values.push(input.posterImageUrl || null); } if (fields.length === 0) return true; diff --git a/src/models/feedback/feedback.interface.ts b/src/models/feedback/feedback.interface.ts index dbbdd5e..5ab0ebd 100644 --- a/src/models/feedback/feedback.interface.ts +++ b/src/models/feedback/feedback.interface.ts @@ -62,6 +62,10 @@ * feedbackDeadline: * type: string * format: date-time + * posterImageUrl: + * type: string + * nullable: true + * example: "https://www.nachklang.art/img/nk/image-20260727-214855-851.jpeg" * EventConfig: * allOf: * - $ref: '#/components/schemas/EventSummary' @@ -116,6 +120,7 @@ export interface EventSummary { subtitle: string | null; eventDate: string; feedbackDeadline: string; + posterImageUrl: string | null; } export interface EventConfig extends EventSummary { diff --git a/src/models/feedback/public/events.public.service.ts b/src/models/feedback/public/events.public.service.ts index cbf6bad..3f8c9ec 100644 --- a/src/models/feedback/public/events.public.service.ts +++ b/src/models/feedback/public/events.public.service.ts @@ -9,7 +9,7 @@ export const getEligibleEvents = async (): Promise => { let conn = await NachklangFeedbackDB.getConnection(); try { const query = ` - SELECT slug, name, subtitle, event_date, feedback_deadline + SELECT slug, name, subtitle, event_date, feedback_deadline, poster_image_url FROM events WHERE is_published = 1 AND event_date <= CURDATE() AND feedback_deadline >= NOW() ORDER BY event_date DESC`; @@ -19,7 +19,8 @@ export const getEligibleEvents = async (): Promise => { name: row.name, subtitle: row.subtitle, eventDate: row.event_date, - feedbackDeadline: row.feedback_deadline + feedbackDeadline: row.feedback_deadline, + posterImageUrl: row.poster_image_url })); } finally { await conn.end(); @@ -41,7 +42,7 @@ export const getEventConfigBySlug = async (slug: string): Promise