feat(events): show total ticket capacity alongside remaining

Availability only showed remaining ("N tickets available"), not the total
capacity. Derive total (remaining + sold) on EventTicketInfo and display
"N of M tickets left" on both the event card and the detail page, so
buyers can gauge demand. Unlimited events are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-16 00:45:41 +02:00
commit f20b404d09
7 changed files with 11 additions and 2 deletions

View file

@ -107,6 +107,7 @@ const messages: LocaleMessages = {
when: 'When',
tickets: 'Tickets',
ticketsAvailable: '{count} tickets available',
ticketsRemainingOfTotal: '{count} of {total} tickets left',
ticketsOwned: 'You have {count} ticket | You have {count} tickets',
unlimitedTickets: 'Unlimited tickets',
buyTicket: 'Buy ticket',

View file

@ -107,6 +107,7 @@ const messages: LocaleMessages = {
when: 'Cuándo',
tickets: 'Boletos',
ticketsAvailable: '{count} boletos disponibles',
ticketsRemainingOfTotal: '{count} de {total} boletos restantes',
ticketsOwned: 'Tienes {count} boleto | Tienes {count} boletos',
unlimitedTickets: 'Boletos ilimitados',
buyTicket: 'Comprar boleto',

View file

@ -107,6 +107,7 @@ const messages: LocaleMessages = {
when: 'Quand',
tickets: 'Billets',
ticketsAvailable: '{count} billets disponibles',
ticketsRemainingOfTotal: '{count} sur {total} billets restants',
ticketsOwned: 'Vous avez {count} billet | Vous avez {count} billets',
unlimitedTickets: 'Billets illimités',
buyTicket: 'Acheter un billet',

View file

@ -82,6 +82,7 @@ export interface LocaleMessages {
when: string
tickets: string
ticketsAvailable: string
ticketsRemainingOfTotal: string
ticketsOwned: string
unlimitedTickets: string
buyTicket: string

View file

@ -245,7 +245,7 @@ const isNonApproved = computed(
{{ t('events.detail.unlimitedTickets', 'Unlimited tickets') }}
</span>
<span v-else-if="event.ticketInfo.available > 0">
{{ t('events.detail.ticketsAvailable', { count: event.ticketInfo.available }) }}
{{ t('events.detail.ticketsRemainingOfTotal', { count: event.ticketInfo.available, total: event.ticketInfo.total }) }}
</span>
<span v-else class="text-destructive font-medium">
{{ t('events.detail.soldOut') }}

View file

@ -78,6 +78,8 @@ export interface EventTicketInfo {
available?: number
/** Running paid count. */
sold: number
/** Total capacity (available + sold). Undefined means unlimited. */
total?: number
/** Whether the organizer enabled fiat checkout. */
allowFiat: boolean
/** Fiat settle currency when allowFiat is true. */
@ -91,6 +93,9 @@ function ticketTagsToInfo(ticket: TicketTags | undefined): EventTicketInfo | und
currency: ticket.currency,
available: ticket.available,
sold: ticket.sold,
// Capacity isn't published directly; derive it from remaining + sold.
// Undefined `available` means unlimited, so total stays undefined too.
total: ticket.available !== undefined ? ticket.available + ticket.sold : undefined,
allowFiat: ticket.allowFiat,
fiatCurrency: ticket.fiatCurrency,
}

View file

@ -347,7 +347,7 @@ function goToMyTickets() {
{{ t('events.detail.unlimitedTickets', 'Unlimited tickets') }}
</span>
<span v-else>
{{ t('events.detail.ticketsAvailable', { count: event.ticketInfo.available }) }}
{{ t('events.detail.ticketsRemainingOfTotal', { count: event.ticketInfo.available, total: event.ticketInfo.total }) }}
</span>
</p>
</div>