Add CreateEventDialog component for event creation functionality

- Introduced CreateEventDialog.vue to facilitate the creation of new events with a comprehensive form for user input.
- Implemented form validation using Vee-Validate and Zod to ensure data integrity.
- Integrated event creation logic in the EventsApiService to handle API requests for creating events.
- Updated EventsPage.vue to include the CreateEventDialog, allowing users to open the dialog and submit event details.

These changes enhance the event management capabilities, providing users with a streamlined interface for creating events.
This commit is contained in:
padreug 2025-09-14 17:17:29 +02:00
parent 8ba4d46767
commit c6a02bf90e
4 changed files with 389 additions and 6 deletions

View file

@ -0,0 +1,306 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { format } from 'date-fns'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Button } from '@/components/ui/button'
import { Calendar, Loader2 } from 'lucide-vue-next'
import { toastService } from '@/core/services/ToastService'
import type { CreateEventRequest } from '../types/event'
// Props
interface Props {
open: boolean
onCreateEvent: (eventData: CreateEventRequest) => Promise<void>
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:open': [value: boolean]
'event-created': []
}>()
// Form validation schema
const formSchema = toTypedSchema(z.object({
wallet: z.string().min(1, "Wallet ID is required"),
name: z.string().min(1, "Event name is required").max(200, "Name too long"),
info: z.string().min(1, "Event description is required").max(2000, "Description too long"),
closing_date: z.string().min(1, "Ticket sale closing date is required"),
event_start_date: z.string().min(1, "Event start date is required"),
event_end_date: z.string().min(1, "Event end date is required"),
currency: z.string().default("sat"),
amount_tickets: z.number().min(1, "Must have at least 1 ticket").max(100000, "Too many tickets"),
price_per_ticket: z.number().min(0, "Price must be 0 or higher"),
banner: z.string().optional(),
}).refine((data) => {
const closingDate = new Date(data.closing_date)
const startDate = new Date(data.event_start_date)
const endDate = new Date(data.event_end_date)
return closingDate <= startDate && startDate <= endDate
}, {
message: "Dates must be in order: closing ≤ start ≤ end",
path: ["event_end_date"]
}))
// Form setup
const form = useForm({
validationSchema: formSchema,
initialValues: {
wallet: '',
name: '',
info: '',
closing_date: '',
event_start_date: '',
event_end_date: '',
currency: 'sat',
amount_tickets: 100,
price_per_ticket: 1000,
banner: ''
}
})
const { resetForm, values, meta } = form
const isFormValid = computed(() => meta.value.valid)
const isLoading = ref(false)
// Get today's date in YYYY-MM-DD format for min date validation
const today = computed(() => format(new Date(), 'yyyy-MM-dd'))
// Form submission
const onSubmit = form.handleSubmit(async (formValues) => {
if (!isFormValid.value) return
isLoading.value = true
try {
await props.onCreateEvent(formValues as CreateEventRequest)
toastService.success('Event created successfully!')
resetForm()
emit('update:open', false)
emit('event-created')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to create event'
toastService.error(errorMessage)
} finally {
isLoading.value = false
}
})
// Handle dialog close
const handleOpenChange = (open: boolean) => {
if (!open && !isLoading.value) {
resetForm()
}
emit('update:open', open)
}
</script>
<template>
<Dialog :open="open" @update:open="handleOpenChange">
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Calendar class="w-5 h-5" />
Create New Event
</DialogTitle>
<DialogDescription>
Create a new event with ticket sales. All fields are required.
</DialogDescription>
</DialogHeader>
<form @submit="onSubmit" class="space-y-6 mt-4">
<!-- Wallet ID -->
<FormField v-slot="{ componentField }" name="wallet">
<FormItem>
<FormLabel>Wallet ID *</FormLabel>
<FormControl>
<Input
placeholder="Enter your LNbits wallet ID"
v-bind="componentField"
/>
</FormControl>
<FormDescription>Your LNbits wallet ID (admin key required)</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<!-- Event Name -->
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Event Name *</FormLabel>
<FormControl>
<Input
placeholder="Enter event name"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- Event Description -->
<FormField v-slot="{ componentField }" name="info">
<FormItem>
<FormLabel>Event Description *</FormLabel>
<FormControl>
<Textarea
placeholder="Describe your event..."
rows="3"
v-bind="componentField"
/>
</FormControl>
<FormDescription>Provide details about your event</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<!-- Date Fields -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField v-slot="{ componentField }" name="closing_date">
<FormItem>
<FormLabel>Ticket Sales Close *</FormLabel>
<FormControl>
<Input
type="date"
:min="today"
v-bind="componentField"
/>
</FormControl>
<FormDescription>When ticket sales end</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="event_start_date">
<FormItem>
<FormLabel>Event Starts *</FormLabel>
<FormControl>
<Input
type="date"
:min="today"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="event_end_date">
<FormItem>
<FormLabel>Event Ends *</FormLabel>
<FormControl>
<Input
type="date"
:min="today"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</div>
<!-- Ticket Details -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField v-slot="{ componentField }" name="amount_tickets">
<FormItem>
<FormLabel>Total Tickets *</FormLabel>
<FormControl>
<Input
type="number"
min="1"
max="100000"
placeholder="100"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="price_per_ticket">
<FormItem>
<FormLabel>Price per Ticket *</FormLabel>
<FormControl>
<Input
type="number"
min="0"
step="0.01"
placeholder="1000"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="currency">
<FormItem>
<FormLabel>Currency</FormLabel>
<FormControl>
<Input
placeholder="sat"
v-bind="componentField"
/>
</FormControl>
<FormDescription>Default: sat</FormDescription>
<FormMessage />
</FormItem>
</FormField>
</div>
<!-- Banner URL (Optional) -->
<FormField v-slot="{ componentField }" name="banner">
<FormItem>
<FormLabel>Banner Image URL (Optional)</FormLabel>
<FormControl>
<Input
type="url"
placeholder="https://example.com/banner.jpg"
v-bind="componentField"
/>
</FormControl>
<FormDescription>URL to an image for your event banner</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<!-- Action Buttons -->
<div class="flex justify-end gap-3 pt-4">
<Button
type="button"
variant="outline"
@click="handleOpenChange(false)"
:disabled="isLoading"
>
Cancel
</Button>
<Button
type="submit"
:disabled="isLoading || !isFormValid"
>
<Loader2 v-if="isLoading" class="w-4 h-4 mr-2 animate-spin" />
{{ isLoading ? 'Creating...' : 'Create Event' }}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</template>

View file

@ -1,5 +1,5 @@
// Events API service for the events module
import type { Event, Ticket } from '../types/event'
import type { Event, Ticket, CreateEventRequest } from '../types/event'
export interface EventsApiConfig {
baseUrl: string
@ -152,4 +152,34 @@ export class EventsApiService {
throw error
}
}
async createEvent(eventData: CreateEventRequest, adminKey: string): Promise<Event> {
try {
const response = await fetch(
`${this.config.baseUrl}/events/api/v1/events`,
{
method: 'POST',
headers: {
'accept': 'application/json',
'Content-Type': 'application/json',
'X-API-KEY': adminKey,
},
body: JSON.stringify(eventData),
}
)
if (!response.ok) {
const error = await response.json()
const errorMessage = typeof error.detail === 'string'
? error.detail
: error.detail[0]?.msg || 'Failed to create event'
throw new Error(errorMessage)
}
return await response.json()
} catch (error) {
console.error('Error creating event:', error)
throw error
}
}
}

View file

@ -27,6 +27,19 @@ export interface Ticket {
reg_timestamp: string
}
export interface CreateEventRequest {
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
banner?: string | null
}
export interface EventsApiError {
detail: Array<{
loc: [string, number]

View file

@ -11,8 +11,12 @@ import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { format } from 'date-fns'
import PurchaseTicketDialog from '../components/PurchaseTicketDialog.vue'
import { RefreshCw, User, LogIn } from 'lucide-vue-next'
import CreateEventDialog from '../components/CreateEventDialog.vue'
import { RefreshCw, User, LogIn, Plus } from 'lucide-vue-next'
import { formatEventPrice } from '@/lib/utils/formatting'
import { injectService } from '@/core/di-container'
import { EVENTS_API_TOKEN } from '../composables/useEvents'
import type { CreateEventRequest } from '../types/event'
// Simple reactive module loading
const { isReady: moduleReady, isLoading: moduleLoading, error: moduleError } = useModuleReady('events')
@ -37,6 +41,9 @@ const selectedEvent = ref<{
currency: string
} | null>(null)
// Create event dialog state
const showCreateDialog = ref(false)
function formatDate(dateStr: string) {
if (!dateStr) return 'Date not available'
@ -68,6 +75,21 @@ function handlePurchaseClick(event: {
function handleRetry() {
window.location.reload()
}
// Create event handler
async function handleCreateEvent(eventData: CreateEventRequest) {
const eventsApi = injectService(EVENTS_API_TOKEN)
if (!eventsApi) {
throw new Error('Events API not available')
}
// Use wallet as admin key for now - in production you'd want proper admin key management
await eventsApi.createEvent(eventData, eventData.wallet)
}
function handleEventCreated() {
refresh?.()
}
</script>
<template>
@ -108,10 +130,16 @@ function handleRetry() {
<span>Please log in to purchase tickets</span>
</div>
</div>
<Button variant="secondary" size="sm" @click="refresh" :disabled="isLoading">
<RefreshCw class="w-4 h-4 mr-2" :class="{ 'animate-spin': isLoading }" />
Refresh
</Button>
<div class="flex gap-2">
<Button v-if="isAuthenticated" variant="default" size="sm" @click="showCreateDialog = true">
<Plus class="w-4 h-4 mr-2" />
Create Event
</Button>
<Button variant="secondary" size="sm" @click="refresh" :disabled="isLoading">
<RefreshCw class="w-4 h-4 mr-2" :class="{ 'animate-spin': isLoading }" />
Refresh
</Button>
</div>
</div>
<Tabs default-value="upcoming" class="w-full">
@ -213,5 +241,11 @@ function handleRetry() {
</Tabs>
<PurchaseTicketDialog v-if="selectedEvent" :event="selectedEvent" v-model:is-open="showPurchaseDialog" />
<CreateEventDialog
:open="showCreateDialog"
@update:open="showCreateDialog = $event"
:on-create-event="handleCreateEvent"
@event-created="handleEventCreated"
/>
</div>
</template>