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.
This commit is contained in:
2026-08-18 22:07:31 +02:00
parent c2ddb11c4c
commit fa34a6c9fb
6 changed files with 34 additions and 8 deletions
@@ -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 <DB_HOST> -u <DB_USER> -p <FEEDBACK_DB> < 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;
@@ -33,6 +33,9 @@
* feedbackDeadline: * feedbackDeadline:
* type: string * type: string
* format: date-time * format: date-time
* posterImageUrl:
* type: string
* nullable: true
* isPublished: * isPublished:
* type: boolean * type: boolean
* submissionCount: * submissionCount:
@@ -87,6 +90,7 @@ export interface EventAdminSummary {
subtitle: string | null; subtitle: string | null;
eventDate: string; eventDate: string;
feedbackDeadline: string; feedbackDeadline: string;
posterImageUrl: string | null;
isPublished: boolean; isPublished: boolean;
submissionCount: number; submissionCount: number;
} }
@@ -110,6 +114,7 @@ export interface CreateEventInput {
eventDate: string; eventDate: string;
feedbackDeadline?: string; feedbackDeadline?: string;
introText?: string; introText?: string;
posterImageUrl?: string;
} }
export interface UpdateEventInput { export interface UpdateEventInput {
@@ -119,6 +124,7 @@ export interface UpdateEventInput {
feedbackDeadline?: string; feedbackDeadline?: string;
isPublished?: boolean; isPublished?: boolean;
introText?: string; introText?: string;
posterImageUrl?: string;
} }
export interface AdminQuestion { export interface AdminQuestion {
@@ -59,6 +59,8 @@ export const eventsAdminRouter = express.Router();
* format: date-time * format: date-time
* introText: * introText:
* type: string * type: string
* posterImageUrl:
* type: string
* responses: * responses:
* 201: * 201:
* description: Created * description: Created
@@ -77,13 +79,13 @@ eventsAdminRouter.get('/', async (req: Request, res: Response) => {
eventsAdminRouter.post('/', async (req: Request, res: Response) => { eventsAdminRouter.post('/', async (req: Request, res: Response) => {
try { try {
const {name, subtitle, eventDate, feedbackDeadline, introText} = req.body || {}; const {name, subtitle, eventDate, feedbackDeadline, introText, posterImageUrl} = req.body || {};
if (!name || !eventDate) { if (!name || !eventDate) {
res.status(400).send({status: 'BAD_REQUEST', message: 'name and eventDate are required'}); res.status(400).send({status: 'BAD_REQUEST', message: 'name and eventDate are required'});
return; return;
} }
const eventId = await EventsAdminService.createEvent( const eventId = await EventsAdminService.createEvent(
{name, subtitle, eventDate, feedbackDeadline, introText}, {name, subtitle, eventDate, feedbackDeadline, introText, posterImageUrl},
res.locals.admin.email res.locals.admin.email
); );
res.status(201).send({eventId}); res.status(201).send({eventId});
@@ -38,6 +38,7 @@ const mapSummaryRow = (row: any): EventAdminSummary => ({
subtitle: row.subtitle, subtitle: row.subtitle,
eventDate: row.event_date, eventDate: row.event_date,
feedbackDeadline: row.feedback_deadline, feedbackDeadline: row.feedback_deadline,
posterImageUrl: row.poster_image_url,
isPublished: !!row.is_published, isPublished: !!row.is_published,
submissionCount: Number(row.submission_count) submissionCount: Number(row.submission_count)
}); });
@@ -94,11 +95,11 @@ export const createEvent = async (input: CreateEventInput, createdByEmail: strin
: computeDefaultDeadline(input.eventDate); : computeDefaultDeadline(input.eventDate);
const query = ` const query = `
INSERT INTO events (slug, name, subtitle, event_date, feedback_deadline, intro_text, created_by_email) INSERT INTO events (slug, name, subtitle, event_date, feedback_deadline, intro_text, poster_image_url, created_by_email)
VALUES (?,?,?,?,?,?,?) RETURNING event_id`; VALUES (?,?,?,?,?,?,?,?) RETURNING event_id`;
const res = await conn.query(query, [ const res = await conn.query(query, [
slug, input.name, input.subtitle || null, input.eventDate, formatDatetime(deadline), slug, input.name, input.subtitle || null, input.eventDate, formatDatetime(deadline),
input.introText || null, createdByEmail input.introText || null, input.posterImageUrl || null, createdByEmail
]); ]);
await conn.commit(); await conn.commit();
return res[0].event_id; 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.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.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.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; if (fields.length === 0) return true;
@@ -62,6 +62,10 @@
* feedbackDeadline: * feedbackDeadline:
* type: string * type: string
* format: date-time * format: date-time
* posterImageUrl:
* type: string
* nullable: true
* example: "https://www.nachklang.art/img/nk/image-20260727-214855-851.jpeg"
* EventConfig: * EventConfig:
* allOf: * allOf:
* - $ref: '#/components/schemas/EventSummary' * - $ref: '#/components/schemas/EventSummary'
@@ -116,6 +120,7 @@ export interface EventSummary {
subtitle: string | null; subtitle: string | null;
eventDate: string; eventDate: string;
feedbackDeadline: string; feedbackDeadline: string;
posterImageUrl: string | null;
} }
export interface EventConfig extends EventSummary { export interface EventConfig extends EventSummary {
@@ -9,7 +9,7 @@ export const getEligibleEvents = async (): Promise<EventSummary[]> => {
let conn = await NachklangFeedbackDB.getConnection(); let conn = await NachklangFeedbackDB.getConnection();
try { try {
const query = ` const query = `
SELECT slug, name, subtitle, event_date, feedback_deadline SELECT slug, name, subtitle, event_date, feedback_deadline, poster_image_url
FROM events FROM events
WHERE is_published = 1 AND event_date <= CURDATE() AND feedback_deadline >= NOW() WHERE is_published = 1 AND event_date <= CURDATE() AND feedback_deadline >= NOW()
ORDER BY event_date DESC`; ORDER BY event_date DESC`;
@@ -19,7 +19,8 @@ export const getEligibleEvents = async (): Promise<EventSummary[]> => {
name: row.name, name: row.name,
subtitle: row.subtitle, subtitle: row.subtitle,
eventDate: row.event_date, eventDate: row.event_date,
feedbackDeadline: row.feedback_deadline feedbackDeadline: row.feedback_deadline,
posterImageUrl: row.poster_image_url
})); }));
} finally { } finally {
await conn.end(); await conn.end();
@@ -41,7 +42,7 @@ export const getEventConfigBySlug = async (slug: string): Promise<EventLookupRes
let conn = await NachklangFeedbackDB.getConnection(); let conn = await NachklangFeedbackDB.getConnection();
try { try {
const eventQuery = ` const eventQuery = `
SELECT event_id, slug, name, subtitle, event_date, feedback_deadline, intro_text, is_published SELECT event_id, slug, name, subtitle, event_date, feedback_deadline, intro_text, poster_image_url, is_published
FROM events WHERE slug = ?`; FROM events WHERE slug = ?`;
const eventRows = await conn.query(eventQuery, [slug]); const eventRows = await conn.query(eventQuery, [slug]);
if (eventRows.length === 0) { if (eventRows.length === 0) {
@@ -91,6 +92,7 @@ export const getEventConfigBySlug = async (slug: string): Promise<EventLookupRes
subtitle: eventRow.subtitle, subtitle: eventRow.subtitle,
eventDate: eventRow.event_date, eventDate: eventRow.event_date,
feedbackDeadline: eventRow.feedback_deadline, feedbackDeadline: eventRow.feedback_deadline,
posterImageUrl: eventRow.poster_image_url,
introText: eventRow.intro_text, introText: eventRow.intro_text,
songs, songs,
questions questions