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
|
// 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 {
|
export interface EventsApiConfig {
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
|
|
@ -152,4 +152,34 @@ export class EventsApiService {
|
||||||
throw error
|
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
|
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 {
|
export interface EventsApiError {
|
||||||
detail: Array<{
|
detail: Array<{
|
||||||
loc: [string, number]
|
loc: [string, number]
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,12 @@ 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 } from 'date-fns'
|
||||||
import PurchaseTicketDialog from '../components/PurchaseTicketDialog.vue'
|
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 { 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
|
// Simple reactive module loading
|
||||||
const { isReady: moduleReady, isLoading: moduleLoading, error: moduleError } = useModuleReady('events')
|
const { isReady: moduleReady, isLoading: moduleLoading, error: moduleError } = useModuleReady('events')
|
||||||
|
|
@ -37,6 +41,9 @@ const selectedEvent = ref<{
|
||||||
currency: string
|
currency: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
|
// Create event dialog state
|
||||||
|
const showCreateDialog = ref(false)
|
||||||
|
|
||||||
function formatDate(dateStr: string) {
|
function formatDate(dateStr: string) {
|
||||||
if (!dateStr) return 'Date not available'
|
if (!dateStr) return 'Date not available'
|
||||||
|
|
||||||
|
|
@ -68,6 +75,21 @@ function handlePurchaseClick(event: {
|
||||||
function handleRetry() {
|
function handleRetry() {
|
||||||
window.location.reload()
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -108,11 +130,17 @@ function handleRetry() {
|
||||||
<span>Please log in to purchase tickets</span>
|
<span>Please log in to purchase tickets</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<Button variant="secondary" size="sm" @click="refresh" :disabled="isLoading">
|
||||||
<RefreshCw class="w-4 h-4 mr-2" :class="{ 'animate-spin': isLoading }" />
|
<RefreshCw class="w-4 h-4 mr-2" :class="{ 'animate-spin': isLoading }" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Tabs default-value="upcoming" class="w-full">
|
<Tabs default-value="upcoming" class="w-full">
|
||||||
<TabsList class="grid w-full grid-cols-2">
|
<TabsList class="grid w-full grid-cols-2">
|
||||||
|
|
@ -213,5 +241,11 @@ function handleRetry() {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<PurchaseTicketDialog v-if="selectedEvent" :event="selectedEvent" v-model:is-open="showPurchaseDialog" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue