Mobile-friendly admin pages, redesigned homepage Next Event card
- Extract shared mobile components (BottomSheet, MoreMenu, Dropdown, etc.) into MobileComponents.tsx - Make admin pages mobile-friendly: bookings, emails, events, faq, payments, tickets, users - Redesign homepage Next Event card with banner image, responsive layout, and updated styling - Fix past events showing on homepage/linktree: use proper Date comparison, auto-unfeature expired events - Add "Over" tag to admin events list for past events - Fix backend FRONTEND_URL for cache revalidation Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,9 +5,7 @@ import Link from 'next/link';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import { eventsApi, Event } from '@/lib/api';
|
||||
import { formatPrice, formatDateLong, formatTime } from '@/lib/utils';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { CalendarIcon, MapPinIcon } from '@heroicons/react/24/outline';
|
||||
import { CalendarIcon, MapPinIcon, ClockIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
interface NextEventSectionProps {
|
||||
initialEvent?: Event | null;
|
||||
@@ -16,11 +14,24 @@ interface NextEventSectionProps {
|
||||
export default function NextEventSection({ initialEvent }: NextEventSectionProps) {
|
||||
const { t, locale } = useLanguage();
|
||||
const [nextEvent, setNextEvent] = useState<Event | null>(initialEvent ?? null);
|
||||
const [loading, setLoading] = useState(!initialEvent);
|
||||
const [loading, setLoading] = useState(initialEvent === undefined);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip fetch if we already have server-provided data
|
||||
if (initialEvent !== undefined) return;
|
||||
if (initialEvent !== undefined) {
|
||||
if (initialEvent) {
|
||||
const endTime = initialEvent.endDatetime || initialEvent.startDatetime;
|
||||
if (new Date(endTime).getTime() <= Date.now()) {
|
||||
setNextEvent(null);
|
||||
setLoading(true);
|
||||
eventsApi.getNextUpcoming()
|
||||
.then(({ event }) => setNextEvent(event))
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false));
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
eventsApi.getNextUpcoming()
|
||||
.then(({ event }) => setNextEvent(event))
|
||||
.catch(console.error)
|
||||
@@ -30,6 +41,15 @@ export default function NextEventSection({ initialEvent }: NextEventSectionProps
|
||||
const formatDate = (dateStr: string) => formatDateLong(dateStr, locale as 'en' | 'es');
|
||||
const fmtTime = (dateStr: string) => formatTime(dateStr, locale as 'en' | 'es');
|
||||
|
||||
const title = nextEvent
|
||||
? (locale === 'es' && nextEvent.titleEs ? nextEvent.titleEs : nextEvent.title)
|
||||
: '';
|
||||
const description = nextEvent
|
||||
? (locale === 'es'
|
||||
? (nextEvent.shortDescriptionEs || nextEvent.descriptionEs || nextEvent.shortDescription || nextEvent.description)
|
||||
: (nextEvent.shortDescription || nextEvent.description))
|
||||
: '';
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
@@ -49,56 +69,72 @@ export default function NextEventSection({ initialEvent }: NextEventSectionProps
|
||||
}
|
||||
|
||||
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>{fmtTime(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>
|
||||
<Link href={`/events/${nextEvent.id}`} className="block group">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-2xl overflow-hidden shadow-lg transition-all duration-300 hover:shadow-2xl hover:scale-[1.01]">
|
||||
<div className="flex flex-col md:flex-row">
|
||||
{/* Banner */}
|
||||
{nextEvent.bannerUrl ? (
|
||||
<div className="relative w-full md:w-2/5 flex-shrink-0">
|
||||
<img
|
||||
src={nextEvent.bannerUrl}
|
||||
alt={title}
|
||||
className="w-full h-48 md:h-full object-cover"
|
||||
/>
|
||||
</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')}
|
||||
) : (
|
||||
<div className="w-full md:w-2/5 flex-shrink-0 h-48 md:h-auto bg-gradient-to-br from-primary-yellow/20 to-secondary-gray flex items-center justify-center">
|
||||
<CalendarIcon className="w-16 h-16 text-gray-300" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 p-5 md:p-8 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="text-xl md:text-2xl font-bold text-primary-dark group-hover:text-brand-navy transition-colors">
|
||||
{title}
|
||||
</h3>
|
||||
{description && (
|
||||
<p className="mt-2 text-sm md:text-base text-gray-600 line-clamp-2">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-4 md:mt-5 space-y-2">
|
||||
<div className="flex items-center gap-2.5 text-gray-700 text-sm">
|
||||
<CalendarIcon className="w-4 h-4 text-primary-yellow flex-shrink-0" />
|
||||
<span>{formatDate(nextEvent.startDatetime)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-gray-700 text-sm">
|
||||
<ClockIcon className="w-4 h-4 text-primary-yellow flex-shrink-0" />
|
||||
<span>{fmtTime(nextEvent.startDatetime)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-gray-700 text-sm">
|
||||
<MapPinIcon className="w-4 h-4 text-primary-yellow flex-shrink-0" />
|
||||
<span>{nextEvent.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 md:mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-2xl md:text-3xl font-bold text-primary-dark">
|
||||
{nextEvent.price === 0
|
||||
? t('events.details.free')
|
||||
: formatPrice(nextEvent.price, nextEvent.currency)}
|
||||
</span>
|
||||
{!nextEvent.externalBookingEnabled && nextEvent.availableSeats != null && (
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
{nextEvent.availableSeats} {t('events.details.spotsLeft')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="inline-flex items-center bg-primary-yellow text-primary-dark font-semibold py-2.5 px-5 rounded-xl text-sm transition-all duration-200 group-hover:bg-yellow-400 flex-shrink-0">
|
||||
{t('common.moreInfo')}
|
||||
</span>
|
||||
</div>
|
||||
<Button size="lg" className="mt-6">
|
||||
{t('common.moreInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user