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:
* 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 {
@@ -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});
@@ -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;
@@ -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 {
@@ -9,7 +9,7 @@ export const getEligibleEvents = async (): Promise<EventSummary[]> => {
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<EventSummary[]> => {
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<EventLookupRes
let conn = await NachklangFeedbackDB.getConnection();
try {
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 = ?`;
const eventRows = await conn.query(eventQuery, [slug]);
if (eventRows.length === 0) {
@@ -91,6 +92,7 @@ export const getEventConfigBySlug = async (slug: string): Promise<EventLookupRes
subtitle: eventRow.subtitle,
eventDate: eventRow.event_date,
feedbackDeadline: eventRow.feedback_deadline,
posterImageUrl: eventRow.poster_image_url,
introText: eventRow.intro_text,
songs,
questions