feat(events): add calendar date visual to My Tickets
Replaces the My-tickets filter that lived on the removed calendar page. A calendar button opens the date-picker popup with per-day dots over the user's own events; picking a day filters the ticket list to it (a removable date chip overrides the upcoming/past toggle while active). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ccaaa6a6c5
commit
a6dee29922
1 changed files with 96 additions and 18 deletions
|
|
@ -10,10 +10,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
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, startOfDay, endOfDay } from 'date-fns'
|
||||||
import { Ticket, User, CreditCard, CheckCircle, Clock, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
import { Ticket, User, CreditCard, CheckCircle, Clock, AlertCircle, ChevronLeft, ChevronRight, CalendarDays, X } from 'lucide-vue-next'
|
||||||
|
import EventCalendarPopup from '../components/EventCalendarPopup.vue'
|
||||||
|
import { useDateLocale } from '../composables/useDateLocale'
|
||||||
|
import type { Event } from '../types/event'
|
||||||
|
|
||||||
const { isAuthenticated, userDisplay } = useAuth()
|
const { isAuthenticated, userDisplay } = useAuth()
|
||||||
|
const { dateLocale } = useDateLocale()
|
||||||
const {
|
const {
|
||||||
tickets,
|
tickets,
|
||||||
groupedTickets,
|
groupedTickets,
|
||||||
|
|
@ -50,10 +54,47 @@ function isGroupPast(eventId: string): boolean {
|
||||||
return end < new Date()
|
return end < new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleGroups = computed(() =>
|
// Calendar popup: visualise the days the user has events. Picking a day
|
||||||
groupedTickets.value.filter(g => isGroupPast(g.eventId) === showPast.value),
|
// filters the ticket list to it (overriding the upcoming/past toggle);
|
||||||
|
// clearing it returns to the toggle.
|
||||||
|
const calendarOpen = ref(false)
|
||||||
|
const selectedDay = ref<Date | null>(null)
|
||||||
|
|
||||||
|
// The user's events (resolved from their ticket groups) — feeds the
|
||||||
|
// calendar popup's per-day dots.
|
||||||
|
const myEvents = computed<Event[]>(() => {
|
||||||
|
const out: Event[] = []
|
||||||
|
for (const g of groupedTickets.value) {
|
||||||
|
const ev = eventsStore.getEventById(g.eventId)
|
||||||
|
if (ev) out.push(ev)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedDayLabel = computed(() =>
|
||||||
|
selectedDay.value
|
||||||
|
? format(selectedDay.value, 'EEE, MMM d', { locale: dateLocale.value })
|
||||||
|
: '',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function isGroupOnDay(eventId: string, day: Date): boolean {
|
||||||
|
const ev = eventsStore.getEventById(eventId)
|
||||||
|
if (!ev) return false
|
||||||
|
const end = ev.endDate ?? ev.startDate
|
||||||
|
return ev.startDate <= endOfDay(day) && end >= startOfDay(day)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectDay(date: Date) {
|
||||||
|
selectedDay.value = date
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleGroups = computed(() => {
|
||||||
|
if (selectedDay.value) {
|
||||||
|
return groupedTickets.value.filter(g => isGroupOnDay(g.eventId, selectedDay.value!))
|
||||||
|
}
|
||||||
|
return groupedTickets.value.filter(g => isGroupPast(g.eventId) === showPast.value)
|
||||||
|
})
|
||||||
|
|
||||||
// Tab counts derived from the visible (past/upcoming-filtered) groups so
|
// Tab counts derived from the visible (past/upcoming-filtered) groups so
|
||||||
// the badges match what's actually shown.
|
// the badges match what's actually shown.
|
||||||
const visibleCounts = computed(() => {
|
const visibleCounts = computed(() => {
|
||||||
|
|
@ -205,10 +246,14 @@ onMounted(async () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="tickets.length > 0">
|
<div v-else-if="tickets.length > 0">
|
||||||
<!-- Upcoming/Past toggle — own row, left-aligned so it clears the
|
<!-- Filter row — own row, left-aligned so it clears the fixed
|
||||||
fixed top-right hamburger menu. Defaults to upcoming so the
|
top-right hamburger menu. Upcoming/Past toggle by default;
|
||||||
list isn't cluttered with events that already happened. -->
|
when a day is picked from the calendar it's replaced by a
|
||||||
<div class="mb-4 inline-flex rounded-md border p-0.5">
|
removable date chip (the day overrides the toggle). The
|
||||||
|
calendar button opens a popup visualising the user's event
|
||||||
|
dates. -->
|
||||||
|
<div class="mb-4 flex items-center gap-2">
|
||||||
|
<div v-if="!selectedDay" class="inline-flex rounded-md border p-0.5">
|
||||||
<Button
|
<Button
|
||||||
:variant="!showPast ? 'default' : 'ghost'"
|
:variant="!showPast ? 'default' : 'ghost'"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -226,6 +271,29 @@ onMounted(async () => {
|
||||||
Past
|
Past
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
v-else
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
class="h-7 gap-1.5"
|
||||||
|
aria-label="Clear day filter"
|
||||||
|
@click="selectedDay = null"
|
||||||
|
>
|
||||||
|
<CalendarDays class="w-3.5 h-3.5" />
|
||||||
|
{{ selectedDayLabel }}
|
||||||
|
<X class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8 shrink-0"
|
||||||
|
:class="{ 'bg-accent text-accent-foreground': selectedDay }"
|
||||||
|
aria-label="Open calendar"
|
||||||
|
@click="calendarOpen = true"
|
||||||
|
>
|
||||||
|
<CalendarDays class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Tabs default-value="all" class="w-full">
|
<Tabs default-value="all" class="w-full">
|
||||||
<TabsList class="grid w-full grid-cols-4">
|
<TabsList class="grid w-full grid-cols-4">
|
||||||
|
|
@ -239,7 +307,7 @@ onMounted(async () => {
|
||||||
<TabsContent value="all">
|
<TabsContent value="all">
|
||||||
<ScrollArea class="h-[600px] w-full pr-4">
|
<ScrollArea class="h-[600px] w-full pr-4">
|
||||||
<div v-if="visibleGroups.length === 0" class="text-center py-8 text-muted-foreground">
|
<div v-if="visibleGroups.length === 0" class="text-center py-8 text-muted-foreground">
|
||||||
{{ showPast ? 'No past tickets' : 'No upcoming tickets' }}
|
{{ selectedDay ? 'No tickets on this day' : (showPast ? 'No past tickets' : 'No upcoming tickets') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<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">
|
<Card v-for="group in visibleGroups" :key="group.eventId" class="flex flex-col">
|
||||||
|
|
@ -414,5 +482,15 @@ onMounted(async () => {
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Calendar popup: dots show the days the user has events; picking
|
||||||
|
one filters the ticket list to that day. -->
|
||||||
|
<EventCalendarPopup
|
||||||
|
v-model:open="calendarOpen"
|
||||||
|
:events="myEvents"
|
||||||
|
title="Your event dates"
|
||||||
|
description="Pick a day to see your tickets for it"
|
||||||
|
@select-date="onSelectDay"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue