import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; import { z } from 'zod'; import { db, dbGet, siteSettings, events } from '../db/index.js'; import { eq, and, gte } from 'drizzle-orm'; import { requireAuth } from '../lib/auth.js'; import { generateId, getNow, toDbBool } from '../lib/utils.js'; interface UserContext { id: string; email: string; name: string; role: string; } const siteSettingsRouter = new Hono<{ Variables: { user: UserContext } }>(); // Validation schema for updating site settings const updateSiteSettingsSchema = z.object({ timezone: z.string().optional(), siteName: z.string().optional(), siteDescription: z.string().optional().nullable(), siteDescriptionEs: z.string().optional().nullable(), contactEmail: z.string().email().optional().nullable().or(z.literal('')), contactPhone: z.string().optional().nullable(), facebookUrl: z.string().url().optional().nullable().or(z.literal('')), instagramUrl: z.string().url().optional().nullable().or(z.literal('')), twitterUrl: z.string().url().optional().nullable().or(z.literal('')), linkedinUrl: z.string().url().optional().nullable().or(z.literal('')), featuredEventId: z.string().optional().nullable(), maintenanceMode: z.boolean().optional(), maintenanceMessage: z.string().optional().nullable(), maintenanceMessageEs: z.string().optional().nullable(), }); // Get site settings (public - needed for frontend timezone) siteSettingsRouter.get('/', async (c) => { const settings = await dbGet( (db as any).select().from(siteSettings).limit(1) ); if (!settings) { // Return default settings if none exist return c.json({ settings: { timezone: 'America/Asuncion', siteName: 'Spanglish', siteDescription: null, siteDescriptionEs: null, contactEmail: null, contactPhone: null, facebookUrl: null, instagramUrl: null, twitterUrl: null, linkedinUrl: null, featuredEventId: null, maintenanceMode: false, maintenanceMessage: null, maintenanceMessageEs: null, }, }); } return c.json({ settings }); }); // Get available timezones siteSettingsRouter.get('/timezones', async (c) => { // Common timezones for Americas (especially relevant for Paraguay) const timezones = [ { value: 'America/Asuncion', label: 'Paraguay (Asunción) - UTC-4/-3' }, { value: 'America/Sao_Paulo', label: 'Brazil (São Paulo) - UTC-3' }, { value: 'America/Buenos_Aires', label: 'Argentina (Buenos Aires) - UTC-3' }, { value: 'America/Santiago', label: 'Chile (Santiago) - UTC-4/-3' }, { value: 'America/Lima', label: 'Peru (Lima) - UTC-5' }, { value: 'America/Bogota', label: 'Colombia (Bogotá) - UTC-5' }, { value: 'America/Caracas', label: 'Venezuela (Caracas) - UTC-4' }, { value: 'America/La_Paz', label: 'Bolivia (La Paz) - UTC-4' }, { value: 'America/Montevideo', label: 'Uruguay (Montevideo) - UTC-3' }, { value: 'America/New_York', label: 'US Eastern - UTC-5/-4' }, { value: 'America/Chicago', label: 'US Central - UTC-6/-5' }, { value: 'America/Denver', label: 'US Mountain - UTC-7/-6' }, { value: 'America/Los_Angeles', label: 'US Pacific - UTC-8/-7' }, { value: 'America/Mexico_City', label: 'Mexico (Mexico City) - UTC-6/-5' }, { value: 'Europe/London', label: 'UK (London) - UTC+0/+1' }, { value: 'Europe/Madrid', label: 'Spain (Madrid) - UTC+1/+2' }, { value: 'Europe/Paris', label: 'France (Paris) - UTC+1/+2' }, { value: 'Europe/Berlin', label: 'Germany (Berlin) - UTC+1/+2' }, { value: 'UTC', label: 'UTC (Coordinated Universal Time)' }, ]; return c.json({ timezones }); }); // Update site settings (admin only) siteSettingsRouter.put('/', requireAuth(['admin']), zValidator('json', updateSiteSettingsSchema), async (c) => { const data = c.req.valid('json'); const user = c.get('user'); const now = getNow(); // Check if settings exist const existing = await dbGet( (db as any).select().from(siteSettings).limit(1) ); if (!existing) { // Create new settings record const id = generateId(); // Validate featured event if provided if (data.featuredEventId) { const featuredEvent = await dbGet( (db as any).select().from(events).where(eq((events as any).id, data.featuredEventId)) ); if (!featuredEvent || featuredEvent.status !== 'published') { return c.json({ error: 'Featured event must exist and be published' }, 400); } } const newSettings = { id, timezone: data.timezone || 'America/Asuncion', siteName: data.siteName || 'Spanglish', siteDescription: data.siteDescription || null, siteDescriptionEs: data.siteDescriptionEs || null, contactEmail: data.contactEmail || null, contactPhone: data.contactPhone || null, facebookUrl: data.facebookUrl || null, instagramUrl: data.instagramUrl || null, twitterUrl: data.twitterUrl || null, linkedinUrl: data.linkedinUrl || null, featuredEventId: data.featuredEventId || null, maintenanceMode: toDbBool(data.maintenanceMode || false), maintenanceMessage: data.maintenanceMessage || null, maintenanceMessageEs: data.maintenanceMessageEs || null, updatedAt: now, updatedBy: user.id, }; await (db as any).insert(siteSettings).values(newSettings); return c.json({ settings: newSettings, message: 'Settings created successfully' }, 201); } // Validate featured event if provided if (data.featuredEventId) { const featuredEvent = await dbGet( (db as any).select().from(events).where(eq((events as any).id, data.featuredEventId)) ); if (!featuredEvent || featuredEvent.status !== 'published') { return c.json({ error: 'Featured event must exist and be published' }, 400); } } // Update existing settings const updateData: Record = { ...data, updatedAt: now, updatedBy: user.id, }; // Convert maintenanceMode boolean to appropriate format for database if (typeof data.maintenanceMode === 'boolean') { updateData.maintenanceMode = toDbBool(data.maintenanceMode); } await (db as any) .update(siteSettings) .set(updateData) .where(eq((siteSettings as any).id, existing.id)); const updated = await dbGet( (db as any).select().from(siteSettings).where(eq((siteSettings as any).id, existing.id)) ); return c.json({ settings: updated, message: 'Settings updated successfully' }); }); // Set featured event (admin only) - convenience endpoint for event editor siteSettingsRouter.put('/featured-event', requireAuth(['admin']), zValidator('json', z.object({ eventId: z.string().nullable(), })), async (c) => { const { eventId } = c.req.valid('json'); const user = c.get('user'); const now = getNow(); // Validate event if provided if (eventId) { const event = await dbGet( (db as any).select().from(events).where(eq((events as any).id, eventId)) ); if (!event) { return c.json({ error: 'Event not found' }, 404); } if (event.status !== 'published') { return c.json({ error: 'Event must be published to be featured' }, 400); } } // Get or create settings const existing = await dbGet( (db as any).select().from(siteSettings).limit(1) ); if (!existing) { // Create new settings record with featured event const id = generateId(); const newSettings = { id, timezone: 'America/Asuncion', siteName: 'Spanglish', featuredEventId: eventId, maintenanceMode: 0, updatedAt: now, updatedBy: user.id, }; await (db as any).insert(siteSettings).values(newSettings); return c.json({ featuredEventId: eventId, message: eventId ? 'Event set as featured' : 'Featured event removed' }); } // Update existing settings await (db as any) .update(siteSettings) .set({ featuredEventId: eventId, updatedAt: now, updatedBy: user.id, }) .where(eq((siteSettings as any).id, existing.id)); return c.json({ featuredEventId: eventId, message: eventId ? 'Event set as featured' : 'Featured event removed' }); }); export default siteSettingsRouter;