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 <cursoragent@cursor.com>
This commit is contained in:
+11
-1
@@ -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' },
|
||||
},
|
||||
|
||||
@@ -294,6 +294,45 @@ async function getEventTicketCount(eventId: string): Promise<number> {
|
||||
return ticketCount?.count || 0;
|
||||
}
|
||||
|
||||
// Get the earliest upcoming published event with ticket counts (ignores featured promotion)
|
||||
async function getNextChronologicalUpcoming(): Promise<any | null> {
|
||||
const now = getNow();
|
||||
const event = await dbGet<any>(
|
||||
(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<any>(
|
||||
(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)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
|
||||
async function getFeaturedEventSlug(): Promise<string | null> {
|
||||
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');
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
|
||||
async function getNextEventSlug(): Promise<string | null> {
|
||||
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');
|
||||
}
|
||||
@@ -58,6 +58,18 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
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,
|
||||
|
||||
@@ -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<Event>) =>
|
||||
|
||||
Reference in New Issue
Block a user