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:
parent
63d636a8a0
commit
de8db6a12b
3 changed files with 341 additions and 141 deletions
|
|
@ -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>
|
||||||
|
|
@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function payWithWallet(paymentRequest: string) {
|
// Generate QR code for ticket
|
||||||
if (!currentUser.value || !userWallets.value.length) {
|
async function generateTicketQRCode(ticketId: string) {
|
||||||
throw new Error('No wallet available for payment')
|
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
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find the first wallet with sufficient balance
|
// Pay with wallet
|
||||||
const wallet = userWallets.value.find((w: Wallet) => w.balance_msat > 0)
|
async function payWithWallet(paymentRequest: string) {
|
||||||
if (!wallet) {
|
const walletWithBalance = userWallets.value.find((wallet: any) => wallet.balance_msat > 0)
|
||||||
|
|
||||||
|
if (!walletWithBalance) {
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = null
|
||||||
|
paymentHash.value = null
|
||||||
|
paymentRequest.value = null
|
||||||
|
qrCode.value = null
|
||||||
|
ticketQRCode.value = null
|
||||||
|
purchasedTicketId.value = null
|
||||||
|
showTicketQR.value = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true
|
// Get the invoice
|
||||||
error.value = ''
|
const invoice = await purchaseTicket(eventId)
|
||||||
|
paymentHash.value = invoice.payment_hash
|
||||||
|
paymentRequest.value = invoice.payment_request
|
||||||
|
|
||||||
// Step 1: Get the invoice
|
// Generate QR code for payment
|
||||||
const result = await purchaseTicket(eventId)
|
await generateQRCode(invoice.payment_request)
|
||||||
|
|
||||||
paymentHash.value = result.payment_hash
|
// Try to pay with wallet if available
|
||||||
paymentRequest.value = result.payment_request
|
|
||||||
await generateQRCode(result.payment_request)
|
|
||||||
|
|
||||||
// Step 2: Try to pay with wallet if user has balance
|
|
||||||
if (hasWalletWithBalance.value) {
|
if (hasWalletWithBalance.value) {
|
||||||
|
isPayingWithWallet.value = true
|
||||||
try {
|
try {
|
||||||
await payWithWallet(result.payment_request)
|
await payWithWallet(invoice.payment_request)
|
||||||
// Payment successful, start monitoring for ticket confirmation
|
// If wallet payment succeeds, proceed to check payment status
|
||||||
startPaymentStatusCheck(eventId, result.payment_hash)
|
await startPaymentStatusCheck(eventId, invoice.payment_hash)
|
||||||
return
|
} catch (walletError) {
|
||||||
} catch (walletPaymentError) {
|
// If wallet payment fails, fall back to manual payment
|
||||||
console.log('Wallet payment failed, showing QR code for manual payment:', walletPaymentError)
|
console.log('Wallet payment failed, falling back to manual payment:', walletError)
|
||||||
// Fall back to QR code payment
|
isPayingWithWallet.value = false
|
||||||
startPaymentStatusCheck(eventId, result.payment_hash)
|
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) {
|
|
||||||
clearInterval(paymentCheckInterval.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check payment status every 2 seconds
|
|
||||||
paymentCheckInterval.value = window.setInterval(async () => {
|
|
||||||
try {
|
try {
|
||||||
const status = await checkPaymentStatus(eventId, paymentHash)
|
const result = await checkPaymentStatus(eventId, hash)
|
||||||
|
|
||||||
if (status.paid) {
|
if (result.paid) {
|
||||||
// Payment successful
|
|
||||||
clearInterval(paymentCheckInterval.value!)
|
|
||||||
paymentCheckInterval.value = null
|
|
||||||
isPaymentPending.value = false
|
isPaymentPending.value = false
|
||||||
|
if (checkInterval) {
|
||||||
|
clearInterval(checkInterval)
|
||||||
|
}
|
||||||
|
|
||||||
toast.success('Payment successful! Your ticket has been purchased.')
|
// Generate ticket QR code
|
||||||
|
if (result.ticket_id) {
|
||||||
|
purchasedTicketId.value = result.ticket_id
|
||||||
|
await generateTicketQRCode(result.ticket_id)
|
||||||
|
showTicketQR.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// Reset payment state
|
toast.success('Ticket purchased successfully!')
|
||||||
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)
|
}
|
||||||
|
|
||||||
|
// Check immediately
|
||||||
|
await checkPayment()
|
||||||
|
|
||||||
|
// Then check every 2 seconds
|
||||||
|
checkInterval = setInterval(checkPayment, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop payment status check
|
||||||
function stopPaymentStatusCheck() {
|
function stopPaymentStatusCheck() {
|
||||||
if (paymentCheckInterval.value) {
|
|
||||||
clearInterval(paymentCheckInterval.value)
|
|
||||||
paymentCheckInterval.value = null
|
|
||||||
}
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
<Badge :variant="getTicketStatus(ticket).status === 'pending' ? 'secondary' : 'default'">
|
<div class="flex items-center gap-2">
|
||||||
{{ getTicketStatus(ticket).label }}
|
<Badge :variant="getTicketStatus(ticket).status === 'pending' ? 'secondary' : 'default'">
|
||||||
</Badge>
|
{{ getTicketStatus(ticket).label }}
|
||||||
|
</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>
|
||||||
<Badge variant="default">
|
<div class="flex items-center gap-2">
|
||||||
{{ getTicketStatus(ticket).label }}
|
<Badge variant="default">
|
||||||
</Badge>
|
{{ getTicketStatus(ticket).label }}
|
||||||
|
</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>
|
||||||
<Badge variant="secondary">
|
<div class="flex items-center gap-2">
|
||||||
{{ getTicketStatus(ticket).label }}
|
<Badge variant="secondary">
|
||||||
</Badge>
|
{{ getTicketStatus(ticket).label }}
|
||||||
|
</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>
|
||||||
<Badge variant="default">
|
<div class="flex items-center gap-2">
|
||||||
{{ getTicketStatus(ticket).label }}
|
<Badge variant="default">
|
||||||
</Badge>
|
{{ getTicketStatus(ticket).label }}
|
||||||
|
</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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue