feat(events): add upcoming/past toggle to My Tickets
My Tickets listed every ticket with no way to separate events that already happened. Add an Upcoming/Past segmented toggle (defaults to upcoming) that filters the grouped tickets by their event's date, with the tab counts (All/Paid/Pending/Registered) derived from the visible set so badges match what's shown. Events not yet resolved from relays stay visible under Upcoming until their date is known. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0b3fc11df2
commit
0d01c65eaf
1 changed files with 68 additions and 17 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useUserTickets } from '../composables/useUserTickets'
|
||||
import { useEvents } from '../composables/useEvents'
|
||||
|
|
@ -16,9 +16,6 @@ import { Ticket, User, CreditCard, CheckCircle, Clock, AlertCircle, ChevronLeft,
|
|||
const { isAuthenticated, userDisplay } = useAuth()
|
||||
const {
|
||||
tickets,
|
||||
paidTickets,
|
||||
pendingTickets,
|
||||
registeredTickets,
|
||||
groupedTickets,
|
||||
isLoading,
|
||||
error,
|
||||
|
|
@ -40,6 +37,36 @@ function eventShortLabel(eventId: string): string {
|
|||
return `Event: ${eventId.slice(0, 8)}…`
|
||||
}
|
||||
|
||||
// Past/upcoming toggle. Defaults to upcoming. An event whose end (or
|
||||
// start, if no end) is before now counts as past; events not yet
|
||||
// resolved from relays are treated as upcoming so their tickets stay
|
||||
// visible until we know otherwise.
|
||||
const showPast = ref(false)
|
||||
|
||||
function isGroupPast(eventId: string): boolean {
|
||||
const ev = eventsStore.getEventById(eventId)
|
||||
if (!ev) return false
|
||||
const end = ev.endDate ?? ev.startDate
|
||||
return end < new Date()
|
||||
}
|
||||
|
||||
const visibleGroups = computed(() =>
|
||||
groupedTickets.value.filter(g => isGroupPast(g.eventId) === showPast.value),
|
||||
)
|
||||
|
||||
// Tab counts derived from the visible (past/upcoming-filtered) groups so
|
||||
// the badges match what's actually shown.
|
||||
const visibleCounts = computed(() => {
|
||||
let all = 0, paid = 0, pending = 0, registered = 0
|
||||
for (const g of visibleGroups.value) {
|
||||
all += g.tickets.length
|
||||
paid += g.paidCount
|
||||
pending += g.pendingCount
|
||||
registered += g.registeredCount
|
||||
}
|
||||
return { all, paid, pending, registered }
|
||||
})
|
||||
|
||||
const qrCodes = ref<Record<string, string>>({})
|
||||
const currentTicketIndex = ref<Record<string, number>>({})
|
||||
|
||||
|
|
@ -140,7 +167,7 @@ onMounted(async () => {
|
|||
|
||||
<template>
|
||||
<div class="container mx-auto py-8 px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex justify-between items-start gap-3 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">
|
||||
|
|
@ -153,6 +180,27 @@ onMounted(async () => {
|
|||
<span>Please log in to view your tickets</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upcoming/Past toggle — defaults to upcoming so the list isn't
|
||||
cluttered with events that already happened. -->
|
||||
<div v-if="isAuthenticated && tickets.length > 0" class="inline-flex shrink-0 rounded-md border p-0.5">
|
||||
<Button
|
||||
:variant="!showPast ? 'default' : 'ghost'"
|
||||
size="sm"
|
||||
class="h-7"
|
||||
@click="showPast = false"
|
||||
>
|
||||
Upcoming
|
||||
</Button>
|
||||
<Button
|
||||
:variant="showPast ? 'default' : 'ghost'"
|
||||
size="sm"
|
||||
class="h-7"
|
||||
@click="showPast = true"
|
||||
>
|
||||
Past
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isAuthenticated" class="text-center py-12">
|
||||
|
|
@ -180,17 +228,20 @@ onMounted(async () => {
|
|||
<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>
|
||||
<TabsTrigger value="all">All ({{ visibleCounts.all }})</TabsTrigger>
|
||||
<TabsTrigger value="paid">Paid ({{ visibleCounts.paid }})</TabsTrigger>
|
||||
<TabsTrigger value="pending">Pending ({{ visibleCounts.pending }})</TabsTrigger>
|
||||
<TabsTrigger value="registered">Registered ({{ visibleCounts.registered }})</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- All Tickets Tab -->
|
||||
<TabsContent value="all">
|
||||
<ScrollArea class="h-[600px] w-full pr-4">
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="group in groupedTickets" :key="group.eventId" class="flex flex-col">
|
||||
<div v-if="visibleGroups.length === 0" class="text-center py-8 text-muted-foreground">
|
||||
{{ showPast ? 'No past tickets' : 'No upcoming tickets' }}
|
||||
</div>
|
||||
<div v-else class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="group in visibleGroups" :key="group.eventId" class="flex flex-col">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<CardTitle class="text-foreground min-w-0 flex-1">
|
||||
|
|
@ -288,9 +339,9 @@ onMounted(async () => {
|
|||
<!-- Paid, Pending, Registered tabs follow the same pattern but filter -->
|
||||
<TabsContent value="paid">
|
||||
<ScrollArea class="h-[600px] w-full pr-4">
|
||||
<div v-if="paidTickets.length === 0" class="text-center py-8 text-muted-foreground">No paid tickets</div>
|
||||
<div v-if="visibleCounts.paid === 0" class="text-center py-8 text-muted-foreground">No paid tickets</div>
|
||||
<div v-else class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="group in groupedTickets.filter(g => g.paidCount > 0)" :key="group.eventId" class="flex flex-col">
|
||||
<Card v-for="group in visibleGroups.filter(g => g.paidCount > 0)" :key="group.eventId" class="flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-foreground min-w-0">
|
||||
<RouterLink
|
||||
|
|
@ -313,9 +364,9 @@ onMounted(async () => {
|
|||
|
||||
<TabsContent value="pending">
|
||||
<ScrollArea class="h-[600px] w-full pr-4">
|
||||
<div v-if="pendingTickets.length === 0" class="text-center py-8 text-muted-foreground">No pending tickets</div>
|
||||
<div v-if="visibleCounts.pending === 0" class="text-center py-8 text-muted-foreground">No pending tickets</div>
|
||||
<div v-else class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="group in groupedTickets.filter(g => g.pendingCount > 0)" :key="group.eventId" class="flex flex-col opacity-75">
|
||||
<Card v-for="group in visibleGroups.filter(g => g.pendingCount > 0)" :key="group.eventId" class="flex flex-col opacity-75">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-foreground min-w-0">
|
||||
<RouterLink
|
||||
|
|
@ -338,9 +389,9 @@ onMounted(async () => {
|
|||
|
||||
<TabsContent value="registered">
|
||||
<ScrollArea class="h-[600px] w-full pr-4">
|
||||
<div v-if="registeredTickets.length === 0" class="text-center py-8 text-muted-foreground">No registered tickets</div>
|
||||
<div v-if="visibleCounts.registered === 0" class="text-center py-8 text-muted-foreground">No registered tickets</div>
|
||||
<div v-else class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="group in groupedTickets.filter(g => g.registeredCount > 0)" :key="group.eventId" class="flex flex-col">
|
||||
<Card v-for="group in visibleGroups.filter(g => g.registeredCount > 0)" :key="group.eventId" class="flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-foreground min-w-0">
|
||||
<RouterLink
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue