feat: add featured event with automatic fallback
- Add featured_event_id to site_settings (schema + migration) - Backend: featured event logic in /events/next/upcoming with auto-unset when event ends - Site settings: PUT supports featuredEventId, add PUT /featured-event for admin - Admin events: Set as featured checkbox in editor, star toggle in list, featured badge - Admin settings: Featured Event section with current event and remove/change links - API: siteSettingsApi.setFeaturedEvent(), Event.isFeatured, SiteSettings.featuredEventId - Homepage/linktree unchanged: still use getNextUpcoming (now returns featured or fallback)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Hono } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { z } from 'zod';
|
||||
import { db, dbGet, dbAll, events, tickets, payments, eventPaymentOverrides, emailLogs, invoices } from '../db/index.js';
|
||||
import { db, dbGet, dbAll, events, tickets, payments, eventPaymentOverrides, emailLogs, invoices, siteSettings } from '../db/index.js';
|
||||
import { eq, desc, and, gte, sql } from 'drizzle-orm';
|
||||
import { requireAuth, getAuthUser } from '../lib/auth.js';
|
||||
import { generateId, getNow, convertBooleansForDb, toDbDate } from '../lib/utils.js';
|
||||
@@ -198,10 +198,92 @@ eventsRouter.get('/:id', async (c) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Get next upcoming event (public)
|
||||
// Helper function to get ticket count for an event
|
||||
async function getEventTicketCount(eventId: string): Promise<number> {
|
||||
const ticketCount = await dbGet<any>(
|
||||
(db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(tickets)
|
||||
.where(
|
||||
and(
|
||||
eq((tickets as any).eventId, eventId),
|
||||
sql`${(tickets as any).status} IN ('confirmed', 'checked_in')`
|
||||
)
|
||||
)
|
||||
);
|
||||
return ticketCount?.count || 0;
|
||||
}
|
||||
|
||||
// Get next upcoming event (public) - returns featured event if valid, otherwise next upcoming
|
||||
eventsRouter.get('/next/upcoming', async (c) => {
|
||||
const now = getNow();
|
||||
|
||||
// First, check if there's a featured event in site settings
|
||||
const settings = await dbGet<any>(
|
||||
(db as any).select().from(siteSettings).limit(1)
|
||||
);
|
||||
|
||||
let featuredEvent = null;
|
||||
let shouldUnsetFeatured = false;
|
||||
|
||||
if (settings?.featuredEventId) {
|
||||
// Get the featured event
|
||||
featuredEvent = await dbGet<any>(
|
||||
(db as any)
|
||||
.select()
|
||||
.from(events)
|
||||
.where(eq((events as any).id, settings.featuredEventId))
|
||||
);
|
||||
|
||||
if (featuredEvent) {
|
||||
// Check if featured event is still valid:
|
||||
// 1. Must be published
|
||||
// 2. Must not have ended (endDatetime >= now, or startDatetime >= now if no endDatetime)
|
||||
const eventEndTime = featuredEvent.endDatetime || featuredEvent.startDatetime;
|
||||
const isPublished = featuredEvent.status === 'published';
|
||||
const hasNotEnded = eventEndTime >= now;
|
||||
|
||||
if (!isPublished || !hasNotEnded) {
|
||||
// Featured event is no longer valid - mark for unsetting
|
||||
shouldUnsetFeatured = true;
|
||||
featuredEvent = null;
|
||||
}
|
||||
} else {
|
||||
// Featured event no longer exists
|
||||
shouldUnsetFeatured = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we need to unset the featured event, do it asynchronously
|
||||
if (shouldUnsetFeatured && settings) {
|
||||
// Unset featured event in background (don't await to avoid blocking response)
|
||||
(db as any)
|
||||
.update(siteSettings)
|
||||
.set({ featuredEventId: null, updatedAt: now })
|
||||
.where(eq((siteSettings as any).id, settings.id))
|
||||
.then(() => {
|
||||
console.log('Featured event auto-cleared (event ended or unpublished)');
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.error('Failed to clear featured event:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// If we have a valid featured event, return it
|
||||
if (featuredEvent) {
|
||||
const bookedCount = await getEventTicketCount(featuredEvent.id);
|
||||
const normalized = normalizeEvent(featuredEvent);
|
||||
return c.json({
|
||||
event: {
|
||||
...normalized,
|
||||
bookedCount,
|
||||
availableSeats: normalized.capacity - bookedCount,
|
||||
isFeatured: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: get the next upcoming published event
|
||||
const event = await dbGet<any>(
|
||||
(db as any)
|
||||
.select()
|
||||
@@ -220,26 +302,14 @@ eventsRouter.get('/next/upcoming', async (c) => {
|
||||
return c.json({ event: null });
|
||||
}
|
||||
|
||||
// Count confirmed AND checked_in tickets (checked_in were previously confirmed)
|
||||
// This ensures check-in doesn't affect capacity/spots_left
|
||||
const ticketCount = await dbGet<any>(
|
||||
(db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(tickets)
|
||||
.where(
|
||||
and(
|
||||
eq((tickets as any).eventId, event.id),
|
||||
sql`${(tickets as any).status} IN ('confirmed', 'checked_in')`
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const bookedCount = await getEventTicketCount(event.id);
|
||||
const normalized = normalizeEvent(event);
|
||||
return c.json({
|
||||
event: {
|
||||
...normalized,
|
||||
bookedCount: ticketCount?.count || 0,
|
||||
availableSeats: normalized.capacity - (ticketCount?.count || 0),
|
||||
bookedCount,
|
||||
availableSeats: normalized.capacity - bookedCount,
|
||||
isFeatured: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user