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
|
|
@ -1,6 +1,6 @@
|
|||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useUserTickets } from '@/composables/useUserTickets'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
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 { Badge } from '@/components/ui/badge'
|
||||
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 {
|
||||
|
|
@ -23,6 +23,10 @@ const {
|
|||
refresh
|
||||
} = useUserTickets()
|
||||
|
||||
// QR code state
|
||||
const qrCodes = ref<Record<string, string>>({})
|
||||
const showQRCode = ref<Record<string, boolean>>({})
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
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' }
|
||||
}
|
||||
|
||||
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 () => {
|
||||
if (isAuthenticated.value) {
|
||||
await refresh()
|
||||
|
|
@ -103,9 +136,19 @@ onMounted(async () => {
|
|||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
||||
<Badge :variant="getTicketStatus(ticket).status === 'pending' ? 'secondary' : 'default'">
|
||||
{{ getTicketStatus(ticket).label }}
|
||||
</Badge>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge :variant="getTicketStatus(ticket).status === 'pending' ? 'secondary' : 'default'">
|
||||
{{ 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>
|
||||
<CardDescription>
|
||||
Event ID: {{ ticket.event }}
|
||||
|
|
@ -132,6 +175,25 @@ onMounted(async () => {
|
|||
<span class="text-muted-foreground">Registered:</span>
|
||||
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -146,9 +208,19 @@ onMounted(async () => {
|
|||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
||||
<Badge variant="default">
|
||||
{{ getTicketStatus(ticket).label }}
|
||||
</Badge>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge variant="default">
|
||||
{{ 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>
|
||||
<CardDescription>
|
||||
Event ID: {{ ticket.event }}
|
||||
|
|
@ -175,6 +247,25 @@ onMounted(async () => {
|
|||
<span class="text-muted-foreground">Registered:</span>
|
||||
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -189,9 +280,19 @@ onMounted(async () => {
|
|||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
||||
<Badge variant="secondary">
|
||||
{{ getTicketStatus(ticket).label }}
|
||||
</Badge>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge variant="secondary">
|
||||
{{ 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>
|
||||
<CardDescription>
|
||||
Event ID: {{ ticket.event }}
|
||||
|
|
@ -214,6 +315,25 @@ onMounted(async () => {
|
|||
<span class="text-muted-foreground">Time:</span>
|
||||
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -228,9 +348,19 @@ onMounted(async () => {
|
|||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-foreground text-sm">Ticket #{{ ticket.id.slice(0, 8) }}</CardTitle>
|
||||
<Badge variant="default">
|
||||
{{ getTicketStatus(ticket).label }}
|
||||
</Badge>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge variant="default">
|
||||
{{ 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>
|
||||
<CardDescription>
|
||||
Event ID: {{ ticket.event }}
|
||||
|
|
@ -257,6 +387,25 @@ onMounted(async () => {
|
|||
<span class="text-muted-foreground">Registered:</span>
|
||||
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue