diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index daeb845..38bb64a 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -57,6 +57,7 @@ const messages: LocaleMessages = { tomorrow: 'Tomorrow', thisWeek: 'This Week', thisMonth: 'This Month', + myTickets: 'My tickets', }, categories: { concert: 'Concert', @@ -96,6 +97,11 @@ const messages: LocaleMessages = { when: 'When', tickets: 'Tickets', ticketsAvailable: '{count} tickets available', + ticketsOwned: 'You have {count} ticket | You have {count} tickets', + unlimitedTickets: 'Unlimited tickets', + buyTicket: 'Buy ticket', + buyAnotherTicket: 'Buy another ticket', + viewMyTickets: 'View in My Tickets', soldOut: 'Sold Out', free: 'Free', }, diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 76a5f56..b2603e9 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -57,6 +57,7 @@ const messages: LocaleMessages = { tomorrow: 'Mañana', thisWeek: 'Esta semana', thisMonth: 'Este mes', + myTickets: 'Mis boletos', }, categories: { concert: 'Concierto', @@ -96,6 +97,11 @@ const messages: LocaleMessages = { when: 'Cuándo', tickets: 'Boletos', ticketsAvailable: '{count} boletos disponibles', + ticketsOwned: 'Tienes {count} boleto | Tienes {count} boletos', + unlimitedTickets: 'Boletos ilimitados', + buyTicket: 'Comprar boleto', + buyAnotherTicket: 'Comprar otro boleto', + viewMyTickets: 'Ver en Mis boletos', soldOut: 'Agotado', free: 'Gratis', }, diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 19b8ece..388f602 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -57,6 +57,7 @@ const messages: LocaleMessages = { tomorrow: 'Demain', thisWeek: 'Cette semaine', thisMonth: 'Ce mois-ci', + myTickets: 'Mes billets', }, categories: { concert: 'Concert', @@ -96,6 +97,11 @@ const messages: LocaleMessages = { when: 'Quand', tickets: 'Billets', ticketsAvailable: '{count} billets disponibles', + ticketsOwned: 'Vous avez {count} billet | Vous avez {count} billets', + unlimitedTickets: 'Billets illimités', + buyTicket: 'Acheter un billet', + buyAnotherTicket: 'Acheter un autre billet', + viewMyTickets: 'Voir dans Mes billets', soldOut: 'Épuisé', free: 'Gratuit', }, diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 0ca44dd..d44f236 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -58,6 +58,7 @@ export interface LocaleMessages { tomorrow: string thisWeek: string thisMonth: string + myTickets: string } categories: Record detail: { @@ -71,6 +72,11 @@ export interface LocaleMessages { when: string tickets: string ticketsAvailable: string + ticketsOwned: string + unlimitedTickets: string + buyTicket: string + buyAnotherTicket: string + viewMyTickets: string soldOut: string free: string } diff --git a/src/modules/activities/components/ActivityCard.vue b/src/modules/activities/components/ActivityCard.vue index d021b10..d16c376 100644 --- a/src/modules/activities/components/ActivityCard.vue +++ b/src/modules/activities/components/ActivityCard.vue @@ -4,9 +4,10 @@ import { useI18n } from 'vue-i18n' import { format } from 'date-fns' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { MapPin, Calendar, Ticket, User } from 'lucide-vue-next' +import { MapPin, Calendar, Ticket, User, CheckCircle2 } from 'lucide-vue-next' import BookmarkButton from './BookmarkButton.vue' import { useDateLocale } from '../composables/useDateLocale' +import { useOwnedTickets } from '../composables/useOwnedTickets' import type { Activity } from '../types/activity' const props = defineProps<{ @@ -19,6 +20,9 @@ const emit = defineEmits<{ const { t } = useI18n() const { dateLocale } = useDateLocale() +const { paidCount } = useOwnedTickets() + +const ownedCount = computed(() => paidCount(props.activity.id)) const dateDisplay = computed(() => { const a = props.activity @@ -155,19 +159,38 @@ const placeholderBg = computed(() => { {{ activity.location }} - +
- + + {{ t('activities.detail.unlimitedTickets', 'Unlimited tickets') }} + + {{ t('activities.detail.ticketsAvailable', { count: activity.ticketInfo.available }) }} {{ t('activities.detail.soldOut') }}
+ + +
+ + + {{ t('activities.detail.ticketsOwned', { count: ownedCount }, ownedCount) }} + +
diff --git a/src/modules/activities/components/PurchaseTicketDialog.vue b/src/modules/activities/components/PurchaseTicketDialog.vue index 22eb1b8..b5b5d3f 100644 --- a/src/modules/activities/components/PurchaseTicketDialog.vue +++ b/src/modules/activities/components/PurchaseTicketDialog.vue @@ -7,7 +7,7 @@ import { useTicketPurchase } from '../composables/useTicketPurchase' import { useAuth } from '@/composables/useAuthService' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import type { TicketApiService } from '../services/TicketApiService' -import { User, Wallet, CreditCard, Zap, Ticket, ExternalLink, Landmark } from 'lucide-vue-next' +import { User, Wallet, CreditCard, Zap, Ticket, ExternalLink, Landmark, Minus, Plus, Copy, Check, Loader2 } from 'lucide-vue-next' import { formatEventPrice, formatWalletBalance } from '@/lib/utils/formatting' import PaymentMethodSelector, { type PaymentMethod as PaymentMethodEntry, @@ -41,6 +41,7 @@ const { isLoading, error, paymentHash, + paymentRequest, qrCode, isPaymentPending, isPayingWithWallet, @@ -48,14 +49,39 @@ const { userWallets, hasWalletWithBalance, purchaseTicketForEvent, + payCurrentInvoiceWithWallet, handleOpenLightningWallet, resetPaymentState, cleanup, - ticketQRCode, - purchasedTicketId, + purchasedTicketIds, showTicketQR } = useTicketPurchase() +const MAX_QUANTITY = 10 +const quantity = ref(1) +const copiedInvoice = ref(false) + +function decreaseQuantity() { + if (quantity.value > 1) quantity.value -= 1 +} +function increaseQuantity() { + if (quantity.value < MAX_QUANTITY) quantity.value += 1 +} + +const totalPrice = computed(() => props.event.price_per_ticket * quantity.value) + +async function copyInvoice() { + if (!paymentRequest.value) return + try { + await navigator.clipboard.writeText(paymentRequest.value) + copiedInvoice.value = true + setTimeout(() => (copiedInvoice.value = false), 1500) + } catch { + // Older browsers / insecure contexts; the Open-in-wallet button + // still works as a fallback. + } +} + const { providers, providerMeta } = useFiatProviders() const { convert } = usePriceConversion() @@ -147,10 +173,13 @@ async function handlePurchase() { const method = selectedMethod.value if (!method) return - // Lightning path: existing composable handles QR + wallet auto-pay. + // Lightning path: the composable just creates the invoice + starts + // polling. The buyer picks "Pay with my LNbits wallet" or "Open in + // external wallet" on the invoice screen (restaurant pattern), so + // no auto-pay here. if (method.rail === 'lightning') { try { - await purchaseTicketForEvent(props.event.id) + await purchaseTicketForEvent(props.event.id, { quantity: quantity.value }) } catch (err) { console.error('Error purchasing ticket:', err) } @@ -177,7 +206,11 @@ async function handlePurchase() { props.event.id, userId, accessToken, - { paymentMethod: 'fiat', fiatProvider: method.provider }, + { + paymentMethod: 'fiat', + fiatProvider: method.provider, + quantity: quantity.value, + }, ) if (!invoice.isFiat || !invoice.fiatPaymentRequest) { fiatError.value = 'Fiat provider did not return a checkout URL.' @@ -206,6 +239,8 @@ function handleClose() { fiatRedirectUrl.value = null fiatProviderLabel.value = null fiatError.value = null + quantity.value = 1 + copiedInvoice.value = false } onUnmounted(() => { @@ -215,14 +250,20 @@ onUnmounted(() => {