Show pre-sale close time as a duration before the event starts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import { eventsApi, Event } from '@/lib/api';
|
||||
import { formatPrice, formatDateLong, formatTime, eventSpotsLeft, isEventSoldOut, isPresaleClosed } from '@/lib/utils';
|
||||
import { formatPrice, formatDateLong, formatTime, eventSpotsLeft, isEventSoldOut, isPresaleClosed, parseDate, formatDurationWords } from '@/lib/utils';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import ShareButtons from '@/components/ShareButtons';
|
||||
@@ -72,6 +72,10 @@ export default function EventDetailClient({ eventId, initialEvent }: EventDetail
|
||||
// Pre-sale closure (server-computed cutoff); same mount guard as isPastEvent
|
||||
const presaleClosed = mounted ? isPresaleClosed(event) : false;
|
||||
const canBook = !isSoldOut && !isCancelled && !isPastEvent && !presaleClosed && (event.status === 'published' || event.status === 'unlisted');
|
||||
// Effective lead time (event override or site default), derived from the server cutoff
|
||||
const presaleLeadMinutes = event.presaleClosesAt
|
||||
? Math.max(0, Math.round((parseDate(event.startDatetime).getTime() - parseDate(event.presaleClosesAt).getTime()) / 60_000))
|
||||
: null;
|
||||
|
||||
// Booking card content - reused for mobile and desktop positions
|
||||
const BookingCardContent = () => (
|
||||
@@ -153,11 +157,13 @@ export default function EventDetailClient({ eventId, initialEvent }: EventDetail
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{canBook && !event.externalBookingEnabled && event.presaleClosesAt && (
|
||||
<p className="mt-3 text-center text-xs text-gray-500">
|
||||
{t('events.details.registrationClosesAt', {
|
||||
date: `${formatDate(event.presaleClosesAt)} ${fmtTime(event.presaleClosesAt)}`,
|
||||
})}
|
||||
{canBook && !event.externalBookingEnabled && presaleLeadMinutes !== null && (
|
||||
<p className="mt-3 text-center text-xs text-gray-400">
|
||||
{presaleLeadMinutes > 0
|
||||
? t('events.details.presaleClosesBefore', {
|
||||
duration: formatDurationWords(presaleLeadMinutes, locale as 'en' | 'es'),
|
||||
})
|
||||
: t('events.details.presaleClosesAtStart')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import DurationInput from '@/components/admin/DurationInput';
|
||||
import { StarIcon, TrashIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import { parseDate, EVENT_TIMEZONE, minutesToDuration } from '@/lib/utils';
|
||||
import { parseDate, EVENT_TIMEZONE, formatDurationWords } from '@/lib/utils';
|
||||
|
||||
interface EventFormData {
|
||||
title: string;
|
||||
@@ -43,12 +43,6 @@ interface PresaleDefaults {
|
||||
|
||||
const FALLBACK_PRESALE_DEFAULTS: PresaleDefaults = { enabled: true, minutesBefore: 120 };
|
||||
|
||||
function describeDuration(minutes: number): string {
|
||||
const { value, unit } = minutesToDuration(minutes);
|
||||
const label = value === 1 ? unit.slice(0, -1) : unit;
|
||||
return `${value} ${label}`;
|
||||
}
|
||||
|
||||
const EMPTY_FORM: EventFormData = {
|
||||
title: '', titleEs: '', slug: '', description: '', descriptionEs: '',
|
||||
shortDescription: '', shortDescriptionEs: '',
|
||||
@@ -425,7 +419,7 @@ export default function EventFormModal({
|
||||
)}
|
||||
<p className="text-xs text-gray-500">
|
||||
Site default: {presaleDefaults.enabled
|
||||
? `closes ${describeDuration(presaleDefaults.minutesBefore)} before the start`
|
||||
? `closes ${formatDurationWords(presaleDefaults.minutesBefore)} before the start`
|
||||
: 'off'}
|
||||
{' '}(Admin › Settings › General).
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user