From f5fe87613fafa40a1c9146d27fb0a95a21a134c5 Mon Sep 17 00:00:00 2001 From: Michilis Date: Sat, 27 Jun 2026 05:35:52 +0000 Subject: [PATCH] Add stable /next and /featured URLs that redirect to the current event. These short links always resolve to the latest upcoming or featured event so they can be shared without updating when events rotate. Co-authored-by: Cursor --- backend/src/index.ts | 12 +++- backend/src/routes/events.ts | 68 +++++++++++++-------- frontend/src/app/(public)/featured/page.tsx | 23 +++++++ frontend/src/app/(public)/next/page.tsx | 23 +++++++ frontend/src/app/sitemap.ts | 12 ++++ frontend/src/lib/api/events.ts | 2 + 6 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 frontend/src/app/(public)/featured/page.tsx create mode 100644 frontend/src/app/(public)/next/page.tsx diff --git a/backend/src/index.ts b/backend/src/index.ts index fe47c13..9532ae7 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -673,11 +673,21 @@ const openApiSpec = { }, }, }, + '/api/events/next': { + get: { + tags: ['Events'], + summary: 'Get next upcoming event (chronological)', + description: 'Get the single earliest upcoming published event, ignoring featured promotion.', + responses: { + 200: { description: 'Next event or null' }, + }, + }, + }, '/api/events/next/upcoming': { get: { tags: ['Events'], summary: 'Get next upcoming event', - description: 'Get the single next upcoming published event.', + description: 'Get the featured event if valid, otherwise the single next upcoming published event.', responses: { 200: { description: 'Next event or null' }, }, diff --git a/backend/src/routes/events.ts b/backend/src/routes/events.ts index 8561381..07fe638 100644 --- a/backend/src/routes/events.ts +++ b/backend/src/routes/events.ts @@ -294,6 +294,45 @@ async function getEventTicketCount(eventId: string): Promise { return ticketCount?.count || 0; } +// Get the earliest upcoming published event with ticket counts (ignores featured promotion) +async function getNextChronologicalUpcoming(): Promise { + const now = getNow(); + const event = await dbGet( + (db as any) + .select() + .from(events) + .where( + and( + eq((events as any).status, 'published'), + gte((events as any).startDatetime, now) + ) + ) + .orderBy((events as any).startDatetime) + .limit(1) + ); + + if (!event) { + return null; + } + + const bookedCount = await getEventTicketCount(event.id); + const normalized = normalizeEvent(event); + return { + ...normalized, + bookedCount, + availableSeats: calculateAvailableSeats(normalized.capacity, bookedCount), + }; +} + +// Get next upcoming event (public) - earliest upcoming published event, ignores featured promotion +eventsRouter.get('/next', async (c) => { + const event = await getNextChronologicalUpcoming(); + if (!event) { + return c.json({ event: null }); + } + return c.json({ event: { ...event, isFeatured: false } }); +}); + // Get next upcoming event (public) - returns featured event if valid, otherwise next upcoming eventsRouter.get('/next/upcoming', async (c) => { const now = getNow(); @@ -357,34 +396,13 @@ eventsRouter.get('/next/upcoming', async (c) => { } // Fallback: get the next upcoming published event - const event = await dbGet( - (db as any) - .select() - .from(events) - .where( - and( - eq((events as any).status, 'published'), - gte((events as any).startDatetime, now) - ) - ) - .orderBy((events as any).startDatetime) - .limit(1) - ); - + const event = await getNextChronologicalUpcoming(); + if (!event) { return c.json({ event: null }); } - - const bookedCount = await getEventTicketCount(event.id); - const normalized = normalizeEvent(event); - return c.json({ - event: { - ...normalized, - bookedCount, - availableSeats: calculateAvailableSeats(normalized.capacity, bookedCount), - isFeatured: false, - }, - }); + + return c.json({ event: { ...event, isFeatured: false } }); }); // Create event (admin/organizer only) diff --git a/frontend/src/app/(public)/featured/page.tsx b/frontend/src/app/(public)/featured/page.tsx new file mode 100644 index 0000000..b5a90df --- /dev/null +++ b/frontend/src/app/(public)/featured/page.tsx @@ -0,0 +1,23 @@ +import { redirect } from 'next/navigation'; + +const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; + +async function getFeaturedEventSlug(): Promise { + try { + const revalidateSeconds = + parseInt(process.env.NEXT_EVENT_REVALIDATE_SECONDS || '3600', 10) || 3600; + const response = await fetch(`${apiUrl}/api/events/next/upcoming`, { + next: { tags: ['next-event'], revalidate: revalidateSeconds }, + }); + if (!response.ok) return null; + const data = await response.json(); + return data.event?.slug || null; + } catch { + return null; + } +} + +export default async function FeaturedEventPage() { + const slug = await getFeaturedEventSlug(); + redirect(slug ? `/events/${slug}` : '/events'); +} diff --git a/frontend/src/app/(public)/next/page.tsx b/frontend/src/app/(public)/next/page.tsx new file mode 100644 index 0000000..65ec2e8 --- /dev/null +++ b/frontend/src/app/(public)/next/page.tsx @@ -0,0 +1,23 @@ +import { redirect } from 'next/navigation'; + +const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; + +async function getNextEventSlug(): Promise { + try { + const revalidateSeconds = + parseInt(process.env.NEXT_EVENT_REVALIDATE_SECONDS || '3600', 10) || 3600; + const response = await fetch(`${apiUrl}/api/events/next`, { + next: { tags: ['next-event'], revalidate: revalidateSeconds }, + }); + if (!response.ok) return null; + const data = await response.json(); + return data.event?.slug || null; + } catch { + return null; + } +} + +export default async function NextEventPage() { + const slug = await getNextEventSlug(); + redirect(slug ? `/events/${slug}` : '/events'); +} diff --git a/frontend/src/app/sitemap.ts b/frontend/src/app/sitemap.ts index c0e5762..70168eb 100644 --- a/frontend/src/app/sitemap.ts +++ b/frontend/src/app/sitemap.ts @@ -58,6 +58,18 @@ export default async function sitemap(): Promise { changeFrequency: 'daily', priority: 0.9, }, + { + url: `${siteUrl}/next`, + lastModified: now, + changeFrequency: 'daily', + priority: 0.8, + }, + { + url: `${siteUrl}/featured`, + lastModified: now, + changeFrequency: 'daily', + priority: 0.8, + }, { url: `${siteUrl}/community`, lastModified: now, diff --git a/frontend/src/lib/api/events.ts b/frontend/src/lib/api/events.ts index f2c420b..470b49d 100644 --- a/frontend/src/lib/api/events.ts +++ b/frontend/src/lib/api/events.ts @@ -11,6 +11,8 @@ export const eventsApi = { getById: (id: string) => fetchApi<{ event: Event }>(`/api/events/${id}`), + getNext: () => fetchApi<{ event: Event | null }>('/api/events/next'), + getNextUpcoming: () => fetchApi<{ event: Event | null }>('/api/events/next/upcoming'), create: (data: Partial) =>