Enhance CreateEventDialog with dynamic currency selection and improved validation

- Integrated currency selection using a dropdown, replacing the static input field for currency.
- Removed the wallet input field, now auto-selecting the preferred wallet for event creation.
- Updated form validation to remove the wallet and closing date fields, ensuring ticket sales close when the event ends.
- Added functionality to load available currencies from the EventsApiService when the dialog opens, improving user experience.

These changes streamline the event creation process and enhance the overall usability of the CreateEventDialog component.
This commit is contained in:
padreug 2025-09-14 18:50:34 +02:00
parent 1544126d17
commit f7ac12bf76
3 changed files with 112 additions and 50 deletions

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
@ -22,8 +22,17 @@ import {
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Calendar, Loader2 } from 'lucide-vue-next'
import { toastService } from '@/core/services/ToastService'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import { EVENTS_API_TOKEN } from '../composables/useEvents'
import type { CreateEventRequest } from '../types/event'
// Props
@ -38,25 +47,23 @@ const emit = defineEmits<{
'event-created': []
}>()
// Form validation schema
// Form validation schema (removed wallet field - will be auto-selected)
// Note: Ticket sales will automatically close when the event ends
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"),
currency: z.string().default("sats"),
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
return startDate <= endDate
}, {
message: "Dates must be in order: closing ≤ start ≤ end",
message: "Event start date must be before or equal to end date",
path: ["event_end_date"]
}))
@ -64,19 +71,42 @@ const formSchema = toTypedSchema(z.object({
const form = useForm({
validationSchema: formSchema,
initialValues: {
wallet: '',
name: '',
info: '',
closing_date: '',
event_start_date: '',
event_end_date: '',
currency: 'sat',
currency: 'sats',
amount_tickets: 100,
price_per_ticket: 1000,
banner: ''
}
})
// Get PaymentService for wallet selection
const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE)
// Get EventsApiService for currency loading
const eventsApi = injectService(EVENTS_API_TOKEN)
// Load available currencies
const availableCurrencies = ref<string[]>(['sats'])
const loadingCurrencies = ref(false)
// Load currencies when dialog opens
watch(() => props.open, async (isOpen) => {
if (isOpen && eventsApi && !loadingCurrencies.value) {
loadingCurrencies.value = true
try {
availableCurrencies.value = await eventsApi.getCurrencies()
} catch (error) {
console.warn('Failed to load currencies:', error)
// Keep default currencies
} finally {
loadingCurrencies.value = false
}
}
})
const { resetForm, values, meta } = form
const isFormValid = computed(() => meta.value.valid)
const isLoading = ref(false)
@ -88,9 +118,27 @@ const today = computed(() => format(new Date(), 'yyyy-MM-dd'))
const onSubmit = form.handleSubmit(async (formValues) => {
if (!isFormValid.value) return
if (!paymentService) {
toastService.error('Payment service not available')
return
}
const preferredWallet = paymentService.getPreferredWallet()
if (!preferredWallet) {
toastService.error('No wallet available. Please connect a wallet first.')
return
}
isLoading.value = true
try {
await props.onCreateEvent(formValues as CreateEventRequest)
// Add the selected wallet ID and set closing_date to event_end_date
const eventData: CreateEventRequest = {
...formValues,
wallet: preferredWallet.id,
closing_date: formValues.event_end_date // Ticket sales close when event ends
}
await props.onCreateEvent(eventData)
toastService.success('Event created successfully!')
resetForm()
emit('update:open', false)
@ -126,21 +174,6 @@ const handleOpenChange = (open: boolean) => {
</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>
@ -172,22 +205,7 @@ const handleOpenChange = (open: boolean) => {
</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>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField v-slot="{ componentField }" name="event_start_date">
<FormItem>
<FormLabel>Event Starts *</FormLabel>
@ -255,12 +273,25 @@ const handleOpenChange = (open: boolean) => {
<FormItem>
<FormLabel>Currency</FormLabel>
<FormControl>
<Input
placeholder="sat"
v-bind="componentField"
/>
<Select v-bind="componentField">
<SelectTrigger>
<SelectValue placeholder="Select currency" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="currency in availableCurrencies"
:key="currency"
:value="currency"
>
{{ currency }}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription>Default: sat</FormDescription>
<FormDescription>
<span v-if="loadingCurrencies">Loading currencies...</span>
<span v-else>Currency for ticket pricing</span>
</FormDescription>
<FormMessage />
</FormItem>
</FormField>

View file

@ -182,4 +182,35 @@ export class EventsApiService {
throw error
}
}
async getCurrencies(): Promise<string[]> {
try {
const response = await fetch(
`${this.config.baseUrl}/api/v1/currencies`,
{
headers: {
'accept': 'application/json',
},
}
)
if (!response.ok) {
// If API call fails, return default currencies
console.warn('Failed to fetch currencies from API, using defaults')
return ['sats']
}
const apiCurrencies = await response.json()
// Combine 'sats' with API currencies, following the pattern from market API
if (Array.isArray(apiCurrencies)) {
return ['sats', ...apiCurrencies.filter((currency: string) => currency !== 'sats')]
}
return ['sats']
} catch (error) {
console.error('Error fetching currencies:', error)
return ['sats']
}
}
}

View file

@ -331,7 +331,7 @@ export class NostrmarketAPI extends BaseService {
* Get available currencies
*/
async getCurrencies(): Promise<string[]> {
const baseCurrencies = ['sat']
const baseCurrencies = ['sats']
try {
const apiCurrencies = await this.request<string[]>(