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"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed, watch } from 'vue'
import { useForm } from 'vee-validate' import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod' import * as z from 'zod'
@ -22,8 +22,17 @@ import {
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Calendar, Loader2 } from 'lucide-vue-next' import { Calendar, Loader2 } from 'lucide-vue-next'
import { toastService } from '@/core/services/ToastService' 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' import type { CreateEventRequest } from '../types/event'
// Props // Props
@ -38,25 +47,23 @@ const emit = defineEmits<{
'event-created': [] '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({ 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"), 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"), 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_start_date: z.string().min(1, "Event start date is required"),
event_end_date: z.string().min(1, "Event end 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"), 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"), price_per_ticket: z.number().min(0, "Price must be 0 or higher"),
banner: z.string().optional(), banner: z.string().optional(),
}).refine((data) => { }).refine((data) => {
const closingDate = new Date(data.closing_date)
const startDate = new Date(data.event_start_date) const startDate = new Date(data.event_start_date)
const endDate = new Date(data.event_end_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"] path: ["event_end_date"]
})) }))
@ -64,19 +71,42 @@ const formSchema = toTypedSchema(z.object({
const form = useForm({ const form = useForm({
validationSchema: formSchema, validationSchema: formSchema,
initialValues: { initialValues: {
wallet: '',
name: '', name: '',
info: '', info: '',
closing_date: '',
event_start_date: '', event_start_date: '',
event_end_date: '', event_end_date: '',
currency: 'sat', currency: 'sats',
amount_tickets: 100, amount_tickets: 100,
price_per_ticket: 1000, price_per_ticket: 1000,
banner: '' 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 { resetForm, values, meta } = form
const isFormValid = computed(() => meta.value.valid) const isFormValid = computed(() => meta.value.valid)
const isLoading = ref(false) const isLoading = ref(false)
@ -88,9 +118,27 @@ const today = computed(() => format(new Date(), 'yyyy-MM-dd'))
const onSubmit = form.handleSubmit(async (formValues) => { const onSubmit = form.handleSubmit(async (formValues) => {
if (!isFormValid.value) return 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 isLoading.value = true
try { 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!') toastService.success('Event created successfully!')
resetForm() resetForm()
emit('update:open', false) emit('update:open', false)
@ -126,21 +174,6 @@ const handleOpenChange = (open: boolean) => {
</DialogHeader> </DialogHeader>
<form @submit="onSubmit" class="space-y-6 mt-4"> <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 --> <!-- Event Name -->
<FormField v-slot="{ componentField }" name="name"> <FormField v-slot="{ componentField }" name="name">
<FormItem> <FormItem>
@ -172,22 +205,7 @@ const handleOpenChange = (open: boolean) => {
</FormField> </FormField>
<!-- Date Fields --> <!-- Date Fields -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 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"> <FormField v-slot="{ componentField }" name="event_start_date">
<FormItem> <FormItem>
<FormLabel>Event Starts *</FormLabel> <FormLabel>Event Starts *</FormLabel>
@ -255,12 +273,25 @@ const handleOpenChange = (open: boolean) => {
<FormItem> <FormItem>
<FormLabel>Currency</FormLabel> <FormLabel>Currency</FormLabel>
<FormControl> <FormControl>
<Input <Select v-bind="componentField">
placeholder="sat" <SelectTrigger>
v-bind="componentField" <SelectValue placeholder="Select currency" />
/> </SelectTrigger>
<SelectContent>
<SelectItem
v-for="currency in availableCurrencies"
:key="currency"
:value="currency"
>
{{ currency }}
</SelectItem>
</SelectContent>
</Select>
</FormControl> </FormControl>
<FormDescription>Default: sat</FormDescription> <FormDescription>
<span v-if="loadingCurrencies">Loading currencies...</span>
<span v-else>Currency for ticket pricing</span>
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>

View file

@ -182,4 +182,35 @@ export class EventsApiService {
throw error 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 * Get available currencies
*/ */
async getCurrencies(): Promise<string[]> { async getCurrencies(): Promise<string[]> {
const baseCurrencies = ['sat'] const baseCurrencies = ['sats']
try { try {
const apiCurrencies = await this.request<string[]>( const apiCurrencies = await this.request<string[]>(