feat(events): Add comprehensive events management with dynamic fetching and UI

- Integrate Reka UI Tabs component for event browsing
- Create useEvents composable for event data management
- Implement events API integration with error handling
- Add events page with upcoming and past events sections
- Configure environment variables for API connection
- Add internationalization support for events navigation
This commit is contained in:
padreug 2025-03-09 17:15:39 +01:00
parent b8868f7971
commit b8c881dea2
14 changed files with 316 additions and 7 deletions

View file

@ -19,6 +19,7 @@ const isOpen = ref(false)
const navigation = computed<NavigationItem[]>(() => [
{ name: t('nav.home'), href: '/' },
{ name: t('nav.events'), href: '/events' },
{ name: t('nav.support'), href: '/support' },
])

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { TabsRootEmits, TabsRootProps } from 'reka-ui'
import { TabsRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<TabsRootProps>()
const emits = defineEmits<TabsRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<TabsRoot v-bind="forwarded">
<slot />
</TabsRoot>
</template>

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { TabsList, type TabsListProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<TabsListProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<TabsList
v-bind="delegatedProps"
:class="cn(
'inline-flex items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
props.class,
)"
>
<slot />
</TabsList>
</template>

View file

@ -0,0 +1,4 @@
export { default as Tabs } from './Tabs.vue'
export { default as TabsContent } from './TabsContent.vue'
export { default as TabsList } from './TabsList.vue'
export { default as TabsTrigger } from './TabsTrigger.vue'

View file

@ -0,0 +1,44 @@
import { ref, computed } from 'vue'
import { useAsyncState } from '@vueuse/core'
import type { Event } from '@/lib/types/event'
import { fetchEvents } from '@/lib/api/events'
export function useEvents() {
const { state: events, isLoading, error, execute: refresh } = useAsyncState(
fetchEvents,
[] as Event[],
{
immediate: true,
resetOnExecute: false,
}
)
const sortedEvents = computed(() => {
return [...events.value].sort((a, b) =>
new Date(b.time).getTime() - new Date(a.time).getTime()
)
})
const upcomingEvents = computed(() => {
const now = new Date()
return sortedEvents.value.filter(event =>
new Date(event.event_start_date) > now
)
})
const pastEvents = computed(() => {
const now = new Date()
return sortedEvents.value.filter(event =>
new Date(event.event_end_date) < now
)
})
return {
events: sortedEvents,
upcomingEvents,
pastEvents,
isLoading,
error,
refresh,
}
}

View file

@ -7,6 +7,7 @@ const messages: LocaleMessages = {
directory: 'Directory',
faq: 'FAQ',
support: 'Support',
events: 'Events',
login: 'Login',
logout: 'Logout'
},

View file

@ -5,6 +5,7 @@ export interface LocaleMessages {
directory: string
faq: string
support: string
events: string
login: string
logout: string
}

28
src/lib/api/events.ts Normal file
View file

@ -0,0 +1,28 @@
import type { Event, EventsApiError } from '../types/event'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://lnbits'
const API_KEY = import.meta.env.VITE_API_KEY
export async function fetchEvents(allWallets = true): Promise<Event[]> {
try {
const response = await fetch(
`${API_BASE_URL}/events/api/v1/events?all_wallets=${allWallets}`,
{
headers: {
'accept': 'application/json',
'X-API-KEY': API_KEY,
},
}
)
if (!response.ok) {
const error: EventsApiError = await response.json()
throw new Error(error.detail[0]?.msg || 'Failed to fetch events')
}
return await response.json() as Event[]
} catch (error) {
console.error('Error fetching events:', error)
throw error
}
}

23
src/lib/types/event.ts Normal file
View file

@ -0,0 +1,23 @@
export interface Event {
id: string
wallet: string
name: string
info: string
closing_date: string
event_start_date: string
event_end_date: string
currency: string
amount_tickets: number
price_per_ticket: number
time: string
sold: number
banner: string | null
}
export interface EventsApiError {
detail: Array<{
loc: [string, number]
msg: string
type: string
}>
}

115
src/pages/events.vue Normal file
View file

@ -0,0 +1,115 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script setup lang="ts">
import { useEvents } from '@/composables/useEvents'
import { Card, CardContent, CardDescription, CardFooter, 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 { format } from 'date-fns'
const { upcomingEvents, pastEvents, isLoading, error, refresh } = useEvents()
function formatDate(dateStr: string) {
return format(new Date(dateStr), 'PPP')
}
</script>
<template>
<div class="container mx-auto py-8 px-4">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">Events</h1>
<Button variant="outline" @click="refresh" :disabled="isLoading">
<i-heroicons-arrow-path class="w-4 h-4 mr-2" :class="{ 'animate-spin': isLoading }" />
Refresh
</Button>
</div>
<Tabs default-value="upcoming" class="w-full">
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="upcoming">Upcoming Events</TabsTrigger>
<TabsTrigger value="past">Past Events</TabsTrigger>
</TabsList>
<div v-if="error" class="mt-4 p-4 bg-destructive/15 text-destructive rounded-lg">
{{ error.message }}
</div>
<TabsContent value="upcoming">
<ScrollArea class="h-[600px] w-full pr-4" v-if="upcomingEvents.length">
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card v-for="event in upcomingEvents" :key="event.id" class="flex flex-col">
<CardHeader>
<CardTitle>{{ event.name }}</CardTitle>
<CardDescription>{{ event.info }}</CardDescription>
</CardHeader>
<CardContent class="flex-grow">
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-muted-foreground">Start Date:</span>
<span>{{ formatDate(event.event_start_date) }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">End Date:</span>
<span>{{ formatDate(event.event_end_date) }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Tickets Available:</span>
<span>{{ event.amount_tickets - event.sold }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Price:</span>
<span>{{ event.price_per_ticket }} {{ event.currency }}</span>
</div>
</div>
</CardContent>
<CardFooter>
<Button class="w-full" :disabled="event.amount_tickets <= event.sold">
Buy Ticket
</Button>
</CardFooter>
</Card>
</div>
</ScrollArea>
<div v-else-if="!isLoading" class="text-center py-8 text-muted-foreground">
No upcoming events found
</div>
</TabsContent>
<TabsContent value="past">
<ScrollArea class="h-[600px] w-full pr-4" v-if="pastEvents.length">
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card v-for="event in pastEvents" :key="event.id" class="flex flex-col opacity-75">
<CardHeader>
<CardTitle>{{ event.name }}</CardTitle>
<CardDescription>{{ event.info }}</CardDescription>
</CardHeader>
<CardContent class="flex-grow">
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-muted-foreground">Start Date:</span>
<span>{{ formatDate(event.event_start_date) }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">End Date:</span>
<span>{{ formatDate(event.event_end_date) }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Total Tickets:</span>
<span>{{ event.amount_tickets }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Tickets Sold:</span>
<span>{{ event.sold }}</span>
</div>
</div>
</CardContent>
</Card>
</div>
</ScrollArea>
<div v-else-if="!isLoading" class="text-center py-8 text-muted-foreground">
No past events found
</div>
</TabsContent>
</Tabs>
</div>
</template>

View file

@ -9,6 +9,14 @@ const router = createRouter({
name: 'home',
component: Home
},
{
path: '/events',
name: 'events',
component: () => import('@/pages/events.vue'),
meta: {
title: 'Events'
}
}
]
})