- Add dbGet/dbAll helper functions for database-agnostic queries - Add toDbBool/convertBooleansForDb for boolean type conversion - Add toDbDate/getNow for timestamp type handling - Add generateId that returns UUID for Postgres, nanoid for SQLite - Update all routes to use compatibility helpers - Add normalizeEvent to return clean number types from Postgres decimal - Add formatPrice utility for consistent price display - Add legal pages admin interface with RichTextEditor - Update carousel images - Add drizzle migration files for PostgreSQL
112 lines
4.0 KiB
TypeScript
112 lines
4.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import { useLanguage } from '@/context/LanguageContext';
|
|
import { eventsApi, Event } from '@/lib/api';
|
|
import { formatPrice } from '@/lib/utils';
|
|
import Button from '@/components/ui/Button';
|
|
import Card from '@/components/ui/Card';
|
|
import { CalendarIcon, MapPinIcon } from '@heroicons/react/24/outline';
|
|
|
|
export default function NextEventSection() {
|
|
const { t, locale } = useLanguage();
|
|
const [nextEvent, setNextEvent] = useState<Event | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
eventsApi.getNextUpcoming()
|
|
.then(({ event }) => setNextEvent(event))
|
|
.catch(console.error)
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
};
|
|
|
|
const formatTime = (dateStr: string) => {
|
|
return new Date(dateStr).toLocaleTimeString(locale === 'es' ? 'es-ES' : 'en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<div className="animate-spin w-8 h-8 border-4 border-primary-yellow border-t-transparent rounded-full mx-auto" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!nextEvent) {
|
|
return (
|
|
<div className="text-center py-12 text-gray-500">
|
|
<CalendarIcon className="w-16 h-16 mx-auto mb-4 text-gray-300" />
|
|
<p className="text-lg">{t('home.nextEvent.noEvents')}</p>
|
|
<p className="mt-2">{t('home.nextEvent.stayTuned')}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Link href={`/events/${nextEvent.id}`} className="block">
|
|
<Card variant="elevated" className="p-8 cursor-pointer hover:shadow-lg transition-shadow">
|
|
<div className="flex flex-col md:flex-row gap-8">
|
|
<div className="flex-1">
|
|
<h3 className="text-2xl font-bold text-primary-dark">
|
|
{locale === 'es' && nextEvent.titleEs ? nextEvent.titleEs : nextEvent.title}
|
|
</h3>
|
|
<p className="mt-3 text-gray-600 whitespace-pre-line">
|
|
{locale === 'es'
|
|
? (nextEvent.shortDescriptionEs || nextEvent.descriptionEs || nextEvent.shortDescription || nextEvent.description)
|
|
: (nextEvent.shortDescription || nextEvent.description)}
|
|
</p>
|
|
|
|
<div className="mt-6 space-y-3">
|
|
<div className="flex items-center gap-3 text-gray-700">
|
|
<CalendarIcon className="w-5 h-5 text-primary-yellow" />
|
|
<span>{formatDate(nextEvent.startDatetime)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3 text-gray-700">
|
|
<span className="w-5 h-5 flex items-center justify-center text-primary-yellow font-bold">
|
|
⏰
|
|
</span>
|
|
<span>{formatTime(nextEvent.startDatetime)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3 text-gray-700">
|
|
<MapPinIcon className="w-5 h-5 text-primary-yellow" />
|
|
<span>{nextEvent.location}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col justify-between items-start md:items-end">
|
|
<div className="text-right">
|
|
<span className="text-3xl font-bold text-primary-dark">
|
|
{nextEvent.price === 0
|
|
? t('events.details.free')
|
|
: formatPrice(nextEvent.price, nextEvent.currency)}
|
|
</span>
|
|
{!nextEvent.externalBookingEnabled && (
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
{nextEvent.availableSeats} {t('events.details.spotsLeft')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<Button size="lg" className="mt-6">
|
|
{t('common.moreInfo')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</Link>
|
|
);
|
|
}
|