feat: Add My Tickets feature with ticket management
- Introduce MyTickets.vue page to display user tickets with filtering options for paid, pending, and registered tickets. - Implement useUserTickets composable for fetching and managing user ticket data. - Update Navbar.vue to include a link to the My Tickets page. - Enhance events API to support fetching user tickets. - Define Ticket type in event.ts for better type safety.
This commit is contained in:
parent
f7450627bc
commit
63d636a8a0
6 changed files with 403 additions and 2 deletions
|
|
@ -6,7 +6,7 @@ import { useTheme } from '@/components/theme-provider'
|
||||||
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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||||
import { Sun, Moon, Menu, X, User, LogOut } from 'lucide-vue-next'
|
import { Sun, Moon, Menu, X, User, LogOut, Ticket } from 'lucide-vue-next'
|
||||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||||
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
||||||
import ProfileDialog from '@/components/auth/ProfileDialog.vue'
|
import ProfileDialog from '@/components/auth/ProfileDialog.vue'
|
||||||
|
|
@ -27,6 +27,7 @@ const showProfileDialog = ref(false)
|
||||||
const navigation = computed<NavigationItem[]>(() => [
|
const navigation = computed<NavigationItem[]>(() => [
|
||||||
{ name: t('nav.home'), href: '/' },
|
{ name: t('nav.home'), href: '/' },
|
||||||
{ name: t('nav.events'), href: '/events' },
|
{ name: t('nav.events'), href: '/events' },
|
||||||
|
{ name: 'My Tickets', href: '/my-tickets' },
|
||||||
{ name: t('nav.support'), href: '/support' },
|
{ name: t('nav.support'), href: '/support' },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
@ -98,6 +99,10 @@ const handleLogout = async () => {
|
||||||
<User class="h-4 w-4" />
|
<User class="h-4 w-4" />
|
||||||
Profile
|
Profile
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="() => router.push('/my-tickets')" class="gap-2">
|
||||||
|
<Ticket class="h-4 w-4" />
|
||||||
|
My Tickets
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem @click="handleLogout" class="gap-2 text-destructive">
|
<DropdownMenuItem @click="handleLogout" class="gap-2 text-destructive">
|
||||||
<LogOut class="h-4 w-4" />
|
<LogOut class="h-4 w-4" />
|
||||||
|
|
|
||||||
77
src/composables/useUserTickets.ts
Normal file
77
src/composables/useUserTickets.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useAsyncState } from '@vueuse/core'
|
||||||
|
import type { Ticket } from '@/lib/types/event'
|
||||||
|
import { fetchUserTickets } from '@/lib/api/events'
|
||||||
|
import { useAuth } from './useAuth'
|
||||||
|
|
||||||
|
export function useUserTickets() {
|
||||||
|
const { isAuthenticated, currentUser } = useAuth()
|
||||||
|
|
||||||
|
const { state: tickets, isLoading, error: asyncError, execute: refresh } = useAsyncState(
|
||||||
|
async () => {
|
||||||
|
if (!isAuthenticated.value || !currentUser.value) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return await fetchUserTickets(currentUser.value.id)
|
||||||
|
},
|
||||||
|
[] as Ticket[],
|
||||||
|
{
|
||||||
|
immediate: false,
|
||||||
|
resetOnExecute: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const error = computed(() => {
|
||||||
|
if (asyncError.value) {
|
||||||
|
return {
|
||||||
|
message: asyncError.value instanceof Error
|
||||||
|
? asyncError.value.message
|
||||||
|
: 'An error occurred while fetching tickets'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortedTickets = computed(() => {
|
||||||
|
return [...tickets.value].sort((a, b) =>
|
||||||
|
new Date(b.time).getTime() - new Date(a.time).getTime()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const paidTickets = computed(() => {
|
||||||
|
return sortedTickets.value.filter(ticket => ticket.paid)
|
||||||
|
})
|
||||||
|
|
||||||
|
const pendingTickets = computed(() => {
|
||||||
|
return sortedTickets.value.filter(ticket => !ticket.paid)
|
||||||
|
})
|
||||||
|
|
||||||
|
const registeredTickets = computed(() => {
|
||||||
|
return sortedTickets.value.filter(ticket => ticket.registered)
|
||||||
|
})
|
||||||
|
|
||||||
|
const unregisteredTickets = computed(() => {
|
||||||
|
return sortedTickets.value.filter(ticket => ticket.paid && !ticket.registered)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load tickets when authenticated
|
||||||
|
const loadTickets = async () => {
|
||||||
|
if (isAuthenticated.value && currentUser.value) {
|
||||||
|
await refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
tickets: sortedTickets,
|
||||||
|
paidTickets,
|
||||||
|
pendingTickets,
|
||||||
|
registeredTickets,
|
||||||
|
unregisteredTickets,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
refresh: loadTickets,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Event, EventsApiError } from '../types/event'
|
import type { Event, EventsApiError, Ticket } from '../types/event'
|
||||||
import { config } from '@/lib/config'
|
import { config } from '@/lib/config'
|
||||||
import { lnbitsAPI } from './lnbits'
|
import { lnbitsAPI } from './lnbits'
|
||||||
|
|
||||||
|
|
@ -131,4 +131,32 @@ export async function checkPaymentStatus(eventId: string, paymentHash: string):
|
||||||
console.error('Error checking payment status:', error)
|
console.error('Error checking payment status:', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchUserTickets(userId: string): Promise<Ticket[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/events/api/v1/tickets/user/${userId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'X-API-KEY': API_KEY,
|
||||||
|
'Authorization': `Bearer ${lnbitsAPI.getAccessToken()}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error: ApiError = await response.json()
|
||||||
|
const errorMessage = typeof error.detail === 'string'
|
||||||
|
? error.detail
|
||||||
|
: error.detail[0]?.msg || 'Failed to fetch user tickets'
|
||||||
|
throw new Error(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user tickets:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,19 @@ export interface Event {
|
||||||
banner: string | null
|
banner: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Ticket {
|
||||||
|
id: string
|
||||||
|
wallet: string
|
||||||
|
event: string
|
||||||
|
name: string | null
|
||||||
|
email: string | null
|
||||||
|
user_id: string | null
|
||||||
|
registered: boolean
|
||||||
|
paid: boolean
|
||||||
|
time: string
|
||||||
|
reg_timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface EventsApiError {
|
export interface EventsApiError {
|
||||||
detail: Array<{
|
detail: Array<{
|
||||||
loc: [string, number]
|
loc: [string, number]
|
||||||
|
|
|
||||||
269
src/pages/MyTickets.vue
Normal file
269
src/pages/MyTickets.vue
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
<!-- eslint-disable vue/multi-word-component-names -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useUserTickets } from '@/composables/useUserTickets'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
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'
|
||||||
|
|
||||||
|
const { isAuthenticated, userDisplay } = useAuth()
|
||||||
|
const {
|
||||||
|
tickets,
|
||||||
|
paidTickets,
|
||||||
|
pendingTickets,
|
||||||
|
registeredTickets,
|
||||||
|
unregisteredTickets,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refresh
|
||||||
|
} = useUserTickets()
|
||||||
|
|
||||||
|
function formatDate(dateStr: string) {
|
||||||
|
return format(new Date(dateStr), 'PPP')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(dateStr: string) {
|
||||||
|
return format(new Date(dateStr), 'HH:mm')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTicketStatus(ticket: any) {
|
||||||
|
if (!ticket.paid) return { status: 'pending', label: 'Payment Pending', icon: Clock, color: 'text-yellow-600' }
|
||||||
|
if (ticket.registered) return { status: 'registered', label: 'Registered', icon: CheckCircle, color: 'text-green-600' }
|
||||||
|
return { status: 'paid', label: 'Paid', icon: CreditCard, color: 'text-blue-600' }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (isAuthenticated.value) {
|
||||||
|
await refresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto py-8 px-4">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h1 class="text-3xl font-bold text-foreground">My Tickets</h1>
|
||||||
|
<div v-if="isAuthenticated && userDisplay" class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<User class="w-4 h-4" />
|
||||||
|
<span>Logged in as {{ userDisplay.name }}</span>
|
||||||
|
<Badge variant="outline" class="text-xs">{{ userDisplay.shortId }}</Badge>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<AlertCircle class="w-4 h-4" />
|
||||||
|
<span>Please log in to view your tickets</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="secondary" size="sm" @click="refresh" :disabled="isLoading">
|
||||||
|
<span v-if="isLoading" class="animate-spin mr-2">⚡</span>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isAuthenticated" class="text-center py-12">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<Ticket class="w-16 h-16 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold mb-2">No Tickets Found</h2>
|
||||||
|
<p class="text-muted-foreground mb-4">Please log in to view your tickets</p>
|
||||||
|
<Button @click="$router.push('/login')">Login</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="mt-4 p-4 bg-destructive/15 text-destructive rounded-lg">
|
||||||
|
{{ error.message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="tickets.length === 0 && !isLoading" class="text-center py-12">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<Ticket class="w-16 h-16 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold mb-2">No Tickets Found</h2>
|
||||||
|
<p class="text-muted-foreground mb-4">You haven't purchased any tickets yet</p>
|
||||||
|
<Button @click="$router.push('/events')">Browse Events</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="tickets.length > 0">
|
||||||
|
<Tabs default-value="all" class="w-full">
|
||||||
|
<TabsList class="grid w-full grid-cols-4">
|
||||||
|
<TabsTrigger value="all">All ({{ tickets.length }})</TabsTrigger>
|
||||||
|
<TabsTrigger value="paid">Paid ({{ paidTickets.length }})</TabsTrigger>
|
||||||
|
<TabsTrigger value="pending">Pending ({{ pendingTickets.length }})</TabsTrigger>
|
||||||
|
<TabsTrigger value="registered">Registered ({{ registeredTickets.length }})</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="all">
|
||||||
|
<ScrollArea class="h-[600px] w-full pr-4">
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card v-for="ticket in tickets" :key="ticket.id" class="flex flex-col">
|
||||||
|
<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>
|
||||||
|
<CardDescription>
|
||||||
|
Event ID: {{ ticket.event }}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex-grow">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Status:</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
||||||
|
<span class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Purchased:</span>
|
||||||
|
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Time:</span>
|
||||||
|
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="ticket.reg_timestamp" class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Registered:</span>
|
||||||
|
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="paid">
|
||||||
|
<ScrollArea class="h-[600px] w-full pr-4">
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card v-for="ticket in paidTickets" :key="ticket.id" class="flex flex-col">
|
||||||
|
<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>
|
||||||
|
<CardDescription>
|
||||||
|
Event ID: {{ ticket.event }}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex-grow">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Status:</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
||||||
|
<span class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Purchased:</span>
|
||||||
|
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Time:</span>
|
||||||
|
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="ticket.reg_timestamp" class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Registered:</span>
|
||||||
|
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="pending">
|
||||||
|
<ScrollArea class="h-[600px] w-full pr-4">
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card v-for="ticket in pendingTickets" :key="ticket.id" class="flex flex-col opacity-75">
|
||||||
|
<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>
|
||||||
|
<CardDescription>
|
||||||
|
Event ID: {{ ticket.event }}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex-grow">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Status:</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
||||||
|
<span class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Created:</span>
|
||||||
|
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Time:</span>
|
||||||
|
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="registered">
|
||||||
|
<ScrollArea class="h-[600px] w-full pr-4">
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card v-for="ticket in registeredTickets" :key="ticket.id" class="flex flex-col">
|
||||||
|
<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>
|
||||||
|
<CardDescription>
|
||||||
|
Event ID: {{ ticket.event }}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex-grow">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Status:</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<component :is="getTicketStatus(ticket).icon" class="w-4 h-4" :class="getTicketStatus(ticket).color" />
|
||||||
|
<span class="text-sm">{{ getTicketStatus(ticket).label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Purchased:</span>
|
||||||
|
<span class="text-sm">{{ formatDate(ticket.time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Time:</span>
|
||||||
|
<span class="text-sm">{{ formatTime(ticket.time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="ticket.reg_timestamp" class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Registered:</span>
|
||||||
|
<span class="text-sm">{{ formatDate(ticket.reg_timestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -30,6 +30,15 @@ const router = createRouter({
|
||||||
title: 'Events',
|
title: 'Events',
|
||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/my-tickets',
|
||||||
|
name: 'my-tickets',
|
||||||
|
component: () => import('@/pages/MyTickets.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'My Tickets',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue