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:
parent
8ba4d46767
commit
c6a02bf90e
4 changed files with 389 additions and 6 deletions
306
src/modules/events/components/CreateEventDialog.vue
Normal file
306
src/modules/events/components/CreateEventDialog.vue
Normal 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>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,11 +130,17 @@ function handleRetry() {
|
|||
<span>Please log in to purchase tickets</span>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<TabsList class="grid w-full grid-cols-2">
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue