feat: Enhance ticket purchasing and management with QR code support

- Update PurchaseTicketDialog.vue to display a ticket QR code after successful purchase and manage its visibility.
- Refactor useTicketPurchase composable to include ticket QR code generation and state management.
- Introduce QR code functionality in MyTickets.vue for displaying ticket QR codes, allowing users to toggle visibility.
- Improve user experience by providing clear feedback on ticket purchase status and QR code availability.
This commit is contained in:
padreug 2025-08-01 21:54:13 +02:00
parent 63d636a8a0
commit de8db6a12b
3 changed files with 341 additions and 141 deletions

View file

@ -40,7 +40,10 @@ const {
purchaseTicketForEvent, purchaseTicketForEvent,
handleOpenLightningWallet, handleOpenLightningWallet,
resetPaymentState, resetPaymentState,
cleanup cleanup,
ticketQRCode,
purchasedTicketId,
showTicketQR
} = useTicketPurchase() } = useTicketPurchase()
async function handlePurchase() { async function handlePurchase() {
@ -179,7 +182,7 @@ onUnmounted(() => {
</div> </div>
<!-- Payment QR Code and Status --> <!-- Payment QR Code and Status -->
<div v-else class="py-4 flex flex-col items-center gap-4"> <div v-else-if="paymentHash && !showTicketQR" class="py-4 flex flex-col items-center gap-4">
<div class="text-center space-y-2"> <div class="text-center space-y-2">
<h3 class="text-lg font-semibold">Payment Required</h3> <h3 class="text-lg font-semibold">Payment Required</h3>
<p v-if="isPayingWithWallet" class="text-sm text-muted-foreground"> <p v-if="isPayingWithWallet" class="text-sm text-muted-foreground">
@ -190,7 +193,7 @@ onUnmounted(() => {
</p> </p>
</div> </div>
<div v-if="!isPayingWithWallet" class="bg-muted/50 rounded-lg p-4"> <div v-if="!isPayingWithWallet && qrCode" class="bg-muted/50 rounded-lg p-4">
<img :src="qrCode" alt="Lightning payment QR code" class="w-64 h-64 mx-auto" /> <img :src="qrCode" alt="Lightning payment QR code" class="w-64 h-64 mx-auto" />
</div> </div>
@ -213,6 +216,32 @@ onUnmounted(() => {
</div> </div>
</div> </div>
</div> </div>
<!-- Ticket QR Code (After Successful Purchase) -->
<div v-else-if="showTicketQR && ticketQRCode" class="py-4 flex flex-col items-center gap-4">
<div class="text-center space-y-2">
<h3 class="text-lg font-semibold text-green-600">Ticket Purchased Successfully!</h3>
<p class="text-sm text-muted-foreground">
Your ticket has been purchased. Here's your ticket QR code:
</p>
</div>
<div class="bg-muted/50 rounded-lg p-4">
<div class="flex flex-col items-center space-y-3">
<img :src="ticketQRCode" alt="Ticket QR code" class="w-48 h-48 border rounded-lg" />
<div class="text-center">
<p class="text-xs text-muted-foreground">Ticket ID</p>
<p class="text-xs font-mono">{{ purchasedTicketId }}</p>
</div>
</div>
</div>
<div class="space-y-3 w-full">
<Button variant="outline" @click="handleClose" class="w-full">
Close
</Button>
</div>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</template> </template>

View file

@ -1,201 +1,218 @@
import { ref, computed } from 'vue' import { ref, computed, onUnmounted } from 'vue'
import { purchaseTicket, checkPaymentStatus, payInvoiceWithWallet } from '@/lib/api/events' import { purchaseTicket, checkPaymentStatus, payInvoiceWithWallet } from '@/lib/api/events'
import { useAuth } from './useAuth' import { useAuth } from './useAuth'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
interface Wallet {
id: string
user: string
name: string
adminkey: string
inkey: string
deleted: boolean
created_at: string
updated_at: string
currency?: string
balance_msat: number
extra?: {
icon: string
color: string
pinned: boolean
}
}
export function useTicketPurchase() { export function useTicketPurchase() {
const { isAuthenticated, currentUser } = useAuth() const { isAuthenticated, currentUser } = useAuth()
// State
const isLoading = ref(false) const isLoading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const paymentHash = ref('') const paymentHash = ref<string | null>(null)
const paymentRequest = ref('') const paymentRequest = ref<string | null>(null)
const qrCode = ref('') const qrCode = ref<string | null>(null)
const isPaymentPending = ref(false) const isPaymentPending = ref(false)
const paymentCheckInterval = ref<number | null>(null)
const isPayingWithWallet = ref(false) const isPayingWithWallet = ref(false)
const canPurchase = computed(() => { // Ticket QR code state
return isAuthenticated.value && !isLoading.value const ticketQRCode = ref<string | null>(null)
}) const purchasedTicketId = ref<string | null>(null)
const showTicketQR = ref(false)
// Computed properties
const canPurchase = computed(() => isAuthenticated.value && currentUser.value)
const userDisplay = computed(() => { const userDisplay = computed(() => {
if (!currentUser.value) return null if (!currentUser.value) return null
return { return {
name: currentUser.value.username || currentUser.value.email || 'Anonymous', name: currentUser.value.username || currentUser.value.id,
id: currentUser.value.id, shortId: currentUser.value.id.slice(0, 8)
shortId: currentUser.value.id.slice(0, 8) + '...' + currentUser.value.id.slice(-8)
} }
}) })
const userWallets = computed(() => { const userWallets = computed(() => currentUser.value?.wallets || [])
if (!currentUser.value) return [] as Wallet[] const hasWalletWithBalance = computed(() =>
return currentUser.value.wallets || [] userWallets.value.some((wallet: any) => wallet.balance_msat > 0)
}) )
const hasWalletWithBalance = computed(() => {
return userWallets.value.some((wallet: Wallet) => wallet.balance_msat > 0)
})
// Generate QR code for Lightning payment
async function generateQRCode(bolt11: string) { async function generateQRCode(bolt11: string) {
try { try {
const QRCode = await import('qrcode') const qrcode = await import('qrcode')
qrCode.value = await QRCode.toDataURL(`lightning:${bolt11}`) const dataUrl = await qrcode.toDataURL(bolt11, {
width: 256,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
})
qrCode.value = dataUrl
} catch (err) { } catch (err) {
console.error('Failed to generate QR code:', err) console.error('Error generating QR code:', err)
error.value = 'Failed to generate QR code'
} }
} }
// Generate QR code for ticket
async function generateTicketQRCode(ticketId: string) {
try {
const qrcode = await import('qrcode')
const ticketUrl = `ticket://${ticketId}`
const dataUrl = await qrcode.toDataURL(ticketUrl, {
width: 128,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
})
ticketQRCode.value = dataUrl
return dataUrl
} catch (error) {
console.error('Error generating ticket QR code:', error)
return null
}
}
// Pay with wallet
async function payWithWallet(paymentRequest: string) { async function payWithWallet(paymentRequest: string) {
if (!currentUser.value || !userWallets.value.length) { const walletWithBalance = userWallets.value.find((wallet: any) => wallet.balance_msat > 0)
throw new Error('No wallet available for payment')
}
// Find the first wallet with sufficient balance if (!walletWithBalance) {
const wallet = userWallets.value.find((w: Wallet) => w.balance_msat > 0)
if (!wallet) {
throw new Error('No wallet with sufficient balance found') throw new Error('No wallet with sufficient balance found')
} }
try { try {
isPayingWithWallet.value = true await payInvoiceWithWallet(paymentRequest, walletWithBalance.id, walletWithBalance.adminkey)
const result = await payInvoiceWithWallet(paymentRequest, wallet.id, wallet.adminkey) return true
} catch (error) {
toast.success(`Payment successful! Fee: ${result.fee_msat} msat`) console.error('Wallet payment failed:', error)
return result throw error
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to pay with wallet'
toast.error(message)
throw err
} finally {
isPayingWithWallet.value = false
} }
} }
// Purchase ticket for event
async function purchaseTicketForEvent(eventId: string) { async function purchaseTicketForEvent(eventId: string) {
if (!isAuthenticated.value) { if (!canPurchase.value) {
error.value = 'Please log in to purchase tickets' throw new Error('User must be authenticated to purchase tickets')
toast.error('Please log in to purchase tickets')
return
} }
try {
isLoading.value = true isLoading.value = true
error.value = '' error.value = null
paymentHash.value = null
paymentRequest.value = null
qrCode.value = null
ticketQRCode.value = null
purchasedTicketId.value = null
showTicketQR.value = false
// Step 1: Get the invoice
const result = await purchaseTicket(eventId)
paymentHash.value = result.payment_hash
paymentRequest.value = result.payment_request
await generateQRCode(result.payment_request)
// Step 2: Try to pay with wallet if user has balance
if (hasWalletWithBalance.value) {
try { try {
await payWithWallet(result.payment_request) // Get the invoice
// Payment successful, start monitoring for ticket confirmation const invoice = await purchaseTicket(eventId)
startPaymentStatusCheck(eventId, result.payment_hash) paymentHash.value = invoice.payment_hash
return paymentRequest.value = invoice.payment_request
} catch (walletPaymentError) {
console.log('Wallet payment failed, showing QR code for manual payment:', walletPaymentError) // Generate QR code for payment
// Fall back to QR code payment await generateQRCode(invoice.payment_request)
startPaymentStatusCheck(eventId, result.payment_hash)
// Try to pay with wallet if available
if (hasWalletWithBalance.value) {
isPayingWithWallet.value = true
try {
await payWithWallet(invoice.payment_request)
// If wallet payment succeeds, proceed to check payment status
await startPaymentStatusCheck(eventId, invoice.payment_hash)
} catch (walletError) {
// If wallet payment fails, fall back to manual payment
console.log('Wallet payment failed, falling back to manual payment:', walletError)
isPayingWithWallet.value = false
await startPaymentStatusCheck(eventId, invoice.payment_hash)
} }
} else { } else {
// No wallet balance, show QR code for manual payment // No wallet balance, proceed with manual payment
startPaymentStatusCheck(eventId, result.payment_hash) await startPaymentStatusCheck(eventId, invoice.payment_hash)
} }
toast.success('Payment request generated successfully')
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : 'Failed to generate payment request' error.value = err instanceof Error ? err.message : 'Failed to purchase ticket'
error.value = message console.error('Error purchasing ticket:', err)
toast.error(message)
throw err
} finally { } finally {
isLoading.value = false isLoading.value = false
} }
} }
function startPaymentStatusCheck(eventId: string, paymentHash: string) { // Start payment status check
async function startPaymentStatusCheck(eventId: string, hash: string) {
isPaymentPending.value = true isPaymentPending.value = true
let checkInterval: NodeJS.Timeout | null = null
// Clear any existing interval const checkPayment = async () => {
if (paymentCheckInterval.value) { try {
clearInterval(paymentCheckInterval.value) const result = await checkPaymentStatus(eventId, hash)
if (result.paid) {
isPaymentPending.value = false
if (checkInterval) {
clearInterval(checkInterval)
} }
// Check payment status every 2 seconds // Generate ticket QR code
paymentCheckInterval.value = window.setInterval(async () => { if (result.ticket_id) {
try { purchasedTicketId.value = result.ticket_id
const status = await checkPaymentStatus(eventId, paymentHash) await generateTicketQRCode(result.ticket_id)
showTicketQR.value = true
}
if (status.paid) { toast.success('Ticket purchased successfully!')
// Payment successful
clearInterval(paymentCheckInterval.value!)
paymentCheckInterval.value = null
isPaymentPending.value = false
toast.success('Payment successful! Your ticket has been purchased.')
// Reset payment state
resetPaymentState()
return { success: true, ticketId: status.ticket_id }
} }
} catch (err) { } catch (err) {
console.error('Error checking payment status:', err) console.error('Error checking payment status:', err)
// Don't show error to user for status checks, just log it
} }
}, 2000)
} }
function stopPaymentStatusCheck() { // Check immediately
if (paymentCheckInterval.value) { await checkPayment()
clearInterval(paymentCheckInterval.value)
paymentCheckInterval.value = null // Then check every 2 seconds
checkInterval = setInterval(checkPayment, 2000)
} }
// Stop payment status check
function stopPaymentStatusCheck() {
isPaymentPending.value = false isPaymentPending.value = false
} }
// Reset payment state
function resetPaymentState() { function resetPaymentState() {
paymentHash.value = '' isLoading.value = false
paymentRequest.value = '' error.value = null
qrCode.value = '' paymentHash.value = null
error.value = '' paymentRequest.value = null
stopPaymentStatusCheck() qrCode.value = null
isPaymentPending.value = false
isPayingWithWallet.value = false
ticketQRCode.value = null
purchasedTicketId.value = null
showTicketQR.value = false
} }
// Open Lightning wallet
function handleOpenLightningWallet() { function handleOpenLightningWallet() {
if (paymentRequest.value) { if (paymentRequest.value) {
window.location.href = `lightning:${paymentRequest.value}` window.open(`lightning:${paymentRequest.value}`, '_blank')
} }
} }
// Cleanup on unmount // Cleanup function
function cleanup() { function cleanup() {
stopPaymentStatusCheck() stopPaymentStatusCheck()
} }
// Lifecycle
onUnmounted(() => {
cleanup()
})
return { return {
// State // State
isLoading, isLoading,
@ -205,6 +222,11 @@ export function useTicketPurchase() {
qrCode, qrCode,
isPaymentPending, isPaymentPending,
isPayingWithWallet, isPayingWithWallet,
ticketQRCode,
purchasedTicketId,
showTicketQR,
// Computed
canPurchase, canPurchase,
userDisplay, userDisplay,
userWallets, userWallets,
@ -212,9 +234,9 @@ export function useTicketPurchase() {
// Actions // Actions
purchaseTicketForEvent, purchaseTicketForEvent,
payWithWallet,
handleOpenLightningWallet, handleOpenLightningWallet,
resetPaymentState, resetPaymentState,
cleanup, cleanup,
generateTicketQRCode
} }
} }

View file

@ -1,6 +1,6 @@
<!-- eslint-disable vue/multi-word-component-names --> <!-- eslint-disable vue/multi-word-component-names -->
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue' import { onMounted, ref } from 'vue'
import { useUserTickets } from '@/composables/useUserTickets' import { useUserTickets } from '@/composables/useUserTickets'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@ -9,7 +9,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { format } from 'date-fns' import { format } from 'date-fns'
import { Ticket, User, Calendar, CreditCard, CheckCircle, Clock, AlertCircle } from 'lucide-vue-next' import { Ticket, User, Calendar, CreditCard, CheckCircle, Clock, AlertCircle, QrCode } from 'lucide-vue-next'
const { isAuthenticated, userDisplay } = useAuth() const { isAuthenticated, userDisplay } = useAuth()
const { const {
@ -23,6 +23,10 @@ const {
refresh refresh
} = useUserTickets() } = useUserTickets()
// QR code state
const qrCodes = ref<Record<string, string>>({})
const showQRCode = ref<Record<string, boolean>>({})
function formatDate(dateStr: string) { function formatDate(dateStr: string) {
return format(new Date(dateStr), 'PPP') return format(new Date(dateStr), 'PPP')
} }
@ -37,6 +41,35 @@ function getTicketStatus(ticket: any) {
return { status: 'paid', label: 'Paid', icon: CreditCard, color: 'text-blue-600' } return { status: 'paid', label: 'Paid', icon: CreditCard, color: 'text-blue-600' }
} }
async function generateQRCode(ticketId: string) {
if (qrCodes.value[ticketId]) return qrCodes.value[ticketId]
try {
const qrcode = await import('qrcode')
const ticketUrl = `ticket://${ticketId}`
const dataUrl = await qrcode.toDataURL(ticketUrl, {
width: 128,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
})
qrCodes.value[ticketId] = dataUrl
return dataUrl
} catch (error) {
console.error('Error generating QR code:', error)
return null
}
}
function toggleQRCode(ticketId: string) {
showQRCode.value[ticketId] = !showQRCode.value[ticketId]
if (showQRCode.value[ticketId] && !qrCodes.value[ticketId]) {
generateQRCode(ticketId)
}
}
onMounted(async () => { onMounted(async () => {
if (isAuthenticated.value) { if (isAuthenticated.value) {
await refresh() await refresh()
@ -103,9 +136,19 @@ onMounted(async () => {
<CardHeader> <CardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle> <CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
<div class="flex items-center gap-2">
<Badge :variant="getTicketStatus(ticket).status === 'pending' ? 'secondary' : 'default'"> <Badge :variant="getTicketStatus(ticket).status === 'pending' ? 'secondary' : 'default'">
{{ getTicketStatus(ticket).label }} {{ getTicketStatus(ticket).label }}
</Badge> </Badge>
<Button
variant="ghost"
size="sm"
@click="toggleQRCode(ticket.id)"
class="h-6 w-6 p-0"
>
<QrCode class="h-4 w-4" />
</Button>
</div>
</div> </div>
<CardDescription> <CardDescription>
Event ID: {{ ticket.event }} Event ID: {{ ticket.event }}
@ -132,6 +175,25 @@ onMounted(async () => {
<span class="text-muted-foreground">Registered:</span> <span class="text-muted-foreground">Registered:</span>
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span> <span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
</div> </div>
<!-- QR Code Section -->
<div v-if="showQRCode[ticket.id]" class="mt-4 pt-4 border-t">
<div class="flex flex-col items-center space-y-2">
<img
v-if="qrCodes[ticket.id]"
:src="qrCodes[ticket.id]"
alt="Ticket QR Code"
class="w-32 h-32 border rounded-lg"
/>
<div v-else class="w-32 h-32 border rounded-lg flex items-center justify-center">
<span class="text-xs text-muted-foreground">Loading...</span>
</div>
<div class="text-center">
<p class="text-xs text-muted-foreground">Ticket ID</p>
<p class="text-xs font-mono">{{ ticket.id }}</p>
</div>
</div>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -146,9 +208,19 @@ onMounted(async () => {
<CardHeader> <CardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle> <CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
<div class="flex items-center gap-2">
<Badge variant="default"> <Badge variant="default">
{{ getTicketStatus(ticket).label }} {{ getTicketStatus(ticket).label }}
</Badge> </Badge>
<Button
variant="ghost"
size="sm"
@click="toggleQRCode(ticket.id)"
class="h-6 w-6 p-0"
>
<QrCode class="h-4 w-4" />
</Button>
</div>
</div> </div>
<CardDescription> <CardDescription>
Event ID: {{ ticket.event }} Event ID: {{ ticket.event }}
@ -175,6 +247,25 @@ onMounted(async () => {
<span class="text-muted-foreground">Registered:</span> <span class="text-muted-foreground">Registered:</span>
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span> <span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
</div> </div>
<!-- QR Code Section -->
<div v-if="showQRCode[ticket.id]" class="mt-4 pt-4 border-t">
<div class="flex flex-col items-center space-y-2">
<img
v-if="qrCodes[ticket.id]"
:src="qrCodes[ticket.id]"
alt="Ticket QR Code"
class="w-32 h-32 border rounded-lg"
/>
<div v-else class="w-32 h-32 border rounded-lg flex items-center justify-center">
<span class="text-xs text-muted-foreground">Loading...</span>
</div>
<div class="text-center">
<p class="text-xs text-muted-foreground">Ticket ID</p>
<p class="text-xs font-mono">{{ ticket.id }}</p>
</div>
</div>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -189,9 +280,19 @@ onMounted(async () => {
<CardHeader> <CardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle> <CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
<div class="flex items-center gap-2">
<Badge variant="secondary"> <Badge variant="secondary">
{{ getTicketStatus(ticket).label }} {{ getTicketStatus(ticket).label }}
</Badge> </Badge>
<Button
variant="ghost"
size="sm"
@click="toggleQRCode(ticket.id)"
class="h-6 w-6 p-0"
>
<QrCode class="h-4 w-4" />
</Button>
</div>
</div> </div>
<CardDescription> <CardDescription>
Event ID: {{ ticket.event }} Event ID: {{ ticket.event }}
@ -214,6 +315,25 @@ onMounted(async () => {
<span class="text-muted-foreground">Time:</span> <span class="text-muted-foreground">Time:</span>
<span class="text-sm">{{ formatTime(ticket.time) }}</span> <span class="text-sm">{{ formatTime(ticket.time) }}</span>
</div> </div>
<!-- QR Code Section -->
<div v-if="showQRCode[ticket.id]" class="mt-4 pt-4 border-t">
<div class="flex flex-col items-center space-y-2">
<img
v-if="qrCodes[ticket.id]"
:src="qrCodes[ticket.id]"
alt="Ticket QR Code"
class="w-32 h-32 border rounded-lg"
/>
<div v-else class="w-32 h-32 border rounded-lg flex items-center justify-center">
<span class="text-xs text-muted-foreground">Loading...</span>
</div>
<div class="text-center">
<p class="text-xs text-muted-foreground">Ticket ID</p>
<p class="text-xs font-mono">{{ ticket.id }}</p>
</div>
</div>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -228,9 +348,19 @@ onMounted(async () => {
<CardHeader> <CardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle> <CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
<div class="flex items-center gap-2">
<Badge variant="default"> <Badge variant="default">
{{ getTicketStatus(ticket).label }} {{ getTicketStatus(ticket).label }}
</Badge> </Badge>
<Button
variant="ghost"
size="sm"
@click="toggleQRCode(ticket.id)"
class="h-6 w-6 p-0"
>
<QrCode class="h-4 w-4" />
</Button>
</div>
</div> </div>
<CardDescription> <CardDescription>
Event ID: {{ ticket.event }} Event ID: {{ ticket.event }}
@ -257,6 +387,25 @@ onMounted(async () => {
<span class="text-muted-foreground">Registered:</span> <span class="text-muted-foreground">Registered:</span>
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span> <span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
</div> </div>
<!-- QR Code Section -->
<div v-if="showQRCode[ticket.id]" class="mt-4 pt-4 border-t">
<div class="flex flex-col items-center space-y-2">
<img
v-if="qrCodes[ticket.id]"
:src="qrCodes[ticket.id]"
alt="Ticket QR Code"
class="w-32 h-32 border rounded-lg"
/>
<div v-else class="w-32 h-32 border rounded-lg flex items-center justify-center">
<span class="text-xs text-muted-foreground">Loading...</span>
</div>
<div class="text-center">
<p class="text-xs text-muted-foreground">Ticket ID</p>
<p class="text-xs font-mono">{{ ticket.id }}</p>
</div>
</div>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>