Enhance market module with new chat and events features
- Introduce chat module with components, services, and composables for real-time messaging. - Implement events module with API service, components, and ticket purchasing functionality. - Update app configuration to include new modules and their respective settings. - Refactor existing components to integrate with the new chat and events features. - Enhance market store and services to support new functionalities and improve order management. - Update routing to accommodate new views for chat and events, ensuring seamless navigation.
This commit is contained in:
parent
519a9003d4
commit
e40ac91417
46 changed files with 6305 additions and 3264 deletions
65
src/modules/events/composables/useEvents.ts
Normal file
65
src/modules/events/composables/useEvents.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { computed } from 'vue'
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import { injectService } from '@/core/di-container'
|
||||
import type { Event } from '../types/event'
|
||||
import type { EventsApiService } from '../services/events-api'
|
||||
|
||||
// Service token for events API
|
||||
export const EVENTS_API_TOKEN = Symbol('eventsApi')
|
||||
|
||||
export function useEvents() {
|
||||
const eventsApi = injectService<EventsApiService>(EVENTS_API_TOKEN)
|
||||
|
||||
if (!eventsApi) {
|
||||
throw new Error('EventsApiService not available. Make sure events module is installed.')
|
||||
}
|
||||
|
||||
const { state: events, isLoading, error: asyncError, execute: refresh } = useAsyncState(
|
||||
() => eventsApi.fetchEvents(),
|
||||
[] as Event[],
|
||||
{
|
||||
immediate: true,
|
||||
resetOnExecute: false,
|
||||
}
|
||||
)
|
||||
|
||||
const error = computed(() => {
|
||||
if (asyncError.value) {
|
||||
return {
|
||||
message: asyncError.value instanceof Error
|
||||
? asyncError.value.message
|
||||
: 'An error occurred while fetching events'
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const sortedEvents = computed(() => {
|
||||
return [...events.value].sort((a, b) =>
|
||||
new Date(b.time).getTime() - new Date(a.time).getTime()
|
||||
)
|
||||
})
|
||||
|
||||
const upcomingEvents = computed(() => {
|
||||
const now = new Date()
|
||||
return sortedEvents.value.filter(event =>
|
||||
new Date(event.event_start_date) > now
|
||||
)
|
||||
})
|
||||
|
||||
const pastEvents = computed(() => {
|
||||
const now = new Date()
|
||||
return sortedEvents.value.filter(event =>
|
||||
new Date(event.event_end_date) < now
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
events: sortedEvents,
|
||||
upcomingEvents,
|
||||
pastEvents,
|
||||
isLoading,
|
||||
error,
|
||||
refresh,
|
||||
}
|
||||
}
|
||||
242
src/modules/events/composables/useTicketPurchase.ts
Normal file
242
src/modules/events/composables/useTicketPurchase.ts
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { purchaseTicket, checkPaymentStatus, payInvoiceWithWallet } from '@/lib/api/events'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
export function useTicketPurchase() {
|
||||
const { isAuthenticated, currentUser } = useAuth()
|
||||
|
||||
// State
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const paymentHash = ref<string | null>(null)
|
||||
const paymentRequest = ref<string | null>(null)
|
||||
const qrCode = ref<string | null>(null)
|
||||
const isPaymentPending = ref(false)
|
||||
const isPayingWithWallet = ref(false)
|
||||
|
||||
// Ticket QR code state
|
||||
const ticketQRCode = ref<string | null>(null)
|
||||
const purchasedTicketId = ref<string | null>(null)
|
||||
const showTicketQR = ref(false)
|
||||
|
||||
// Computed properties
|
||||
const canPurchase = computed(() => isAuthenticated.value && currentUser.value)
|
||||
const userDisplay = computed(() => {
|
||||
if (!currentUser.value) return null
|
||||
return {
|
||||
name: currentUser.value.username || currentUser.value.id,
|
||||
shortId: currentUser.value.id.slice(0, 8)
|
||||
}
|
||||
})
|
||||
|
||||
const userWallets = computed(() => currentUser.value?.wallets || [])
|
||||
const hasWalletWithBalance = computed(() =>
|
||||
userWallets.value.some((wallet: any) => wallet.balance_msat > 0)
|
||||
)
|
||||
|
||||
// Generate QR code for Lightning payment
|
||||
async function generateQRCode(bolt11: string) {
|
||||
try {
|
||||
const qrcode = await import('qrcode')
|
||||
const dataUrl = await qrcode.toDataURL(bolt11, {
|
||||
width: 256,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF'
|
||||
}
|
||||
})
|
||||
qrCode.value = dataUrl
|
||||
} catch (err) {
|
||||
console.error('Error generating QR code:', err)
|
||||
error.value = 'Failed to generate QR code'
|
||||
}
|
||||
}
|
||||
|
||||
// Generate QR code for ticket
|
||||
async function generateTicketQRCode(ticketId: string) {
|
||||
try {
|
||||
const qrcode = await import('qrcode')
|
||||
const ticketUrl = `ticket://${ticketId}`
|
||||
const dataUrl = await qrcode.toDataURL(ticketUrl, {
|
||||
width: 128,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF'
|
||||
}
|
||||
})
|
||||
ticketQRCode.value = dataUrl
|
||||
return dataUrl
|
||||
} catch (error) {
|
||||
console.error('Error generating ticket QR code:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Pay with wallet
|
||||
async function payWithWallet(paymentRequest: string) {
|
||||
const walletWithBalance = userWallets.value.find((wallet: any) => wallet.balance_msat > 0)
|
||||
|
||||
if (!walletWithBalance) {
|
||||
throw new Error('No wallet with sufficient balance found')
|
||||
}
|
||||
|
||||
try {
|
||||
await payInvoiceWithWallet(paymentRequest, walletWithBalance.id, walletWithBalance.adminkey)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Wallet payment failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Purchase ticket for event
|
||||
async function purchaseTicketForEvent(eventId: string) {
|
||||
if (!canPurchase.value) {
|
||||
throw new Error('User must be authenticated to purchase tickets')
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
paymentHash.value = null
|
||||
paymentRequest.value = null
|
||||
qrCode.value = null
|
||||
ticketQRCode.value = null
|
||||
purchasedTicketId.value = null
|
||||
showTicketQR.value = false
|
||||
|
||||
try {
|
||||
// Get the invoice
|
||||
const invoice = await purchaseTicket(eventId)
|
||||
paymentHash.value = invoice.payment_hash
|
||||
paymentRequest.value = invoice.payment_request
|
||||
|
||||
// Generate QR code for payment
|
||||
await generateQRCode(invoice.payment_request)
|
||||
|
||||
// Try to pay with wallet if available
|
||||
if (hasWalletWithBalance.value) {
|
||||
isPayingWithWallet.value = true
|
||||
try {
|
||||
await payWithWallet(invoice.payment_request)
|
||||
// If wallet payment succeeds, proceed to check payment status
|
||||
await startPaymentStatusCheck(eventId, invoice.payment_hash)
|
||||
} catch (walletError) {
|
||||
// If wallet payment fails, fall back to manual payment
|
||||
console.log('Wallet payment failed, falling back to manual payment:', walletError)
|
||||
isPayingWithWallet.value = false
|
||||
await startPaymentStatusCheck(eventId, invoice.payment_hash)
|
||||
}
|
||||
} else {
|
||||
// No wallet balance, proceed with manual payment
|
||||
await startPaymentStatusCheck(eventId, invoice.payment_hash)
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to purchase ticket'
|
||||
console.error('Error purchasing ticket:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Start payment status check
|
||||
async function startPaymentStatusCheck(eventId: string, hash: string) {
|
||||
isPaymentPending.value = true
|
||||
let checkInterval: number | null = null
|
||||
|
||||
const checkPayment = async () => {
|
||||
try {
|
||||
const result = await checkPaymentStatus(eventId, hash)
|
||||
|
||||
if (result.paid) {
|
||||
isPaymentPending.value = false
|
||||
if (checkInterval) {
|
||||
clearInterval(checkInterval)
|
||||
}
|
||||
|
||||
// Generate ticket QR code
|
||||
if (result.ticket_id) {
|
||||
purchasedTicketId.value = result.ticket_id
|
||||
await generateTicketQRCode(result.ticket_id)
|
||||
showTicketQR.value = true
|
||||
}
|
||||
|
||||
toast.success('Ticket purchased successfully!')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error checking payment status:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check immediately
|
||||
await checkPayment()
|
||||
|
||||
// Then check every 2 seconds
|
||||
checkInterval = setInterval(checkPayment, 2000) as unknown as number
|
||||
}
|
||||
|
||||
// Stop payment status check
|
||||
function stopPaymentStatusCheck() {
|
||||
isPaymentPending.value = false
|
||||
}
|
||||
|
||||
// Reset payment state
|
||||
function resetPaymentState() {
|
||||
isLoading.value = false
|
||||
error.value = null
|
||||
paymentHash.value = null
|
||||
paymentRequest.value = null
|
||||
qrCode.value = null
|
||||
isPaymentPending.value = false
|
||||
isPayingWithWallet.value = false
|
||||
ticketQRCode.value = null
|
||||
purchasedTicketId.value = null
|
||||
showTicketQR.value = false
|
||||
}
|
||||
|
||||
// Open Lightning wallet
|
||||
function handleOpenLightningWallet() {
|
||||
if (paymentRequest.value) {
|
||||
window.open(`lightning:${paymentRequest.value}`, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
function cleanup() {
|
||||
stopPaymentStatusCheck()
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
error,
|
||||
paymentHash,
|
||||
paymentRequest,
|
||||
qrCode,
|
||||
isPaymentPending,
|
||||
isPayingWithWallet,
|
||||
ticketQRCode,
|
||||
purchasedTicketId,
|
||||
showTicketQR,
|
||||
|
||||
// Computed
|
||||
canPurchase,
|
||||
userDisplay,
|
||||
userWallets,
|
||||
hasWalletWithBalance,
|
||||
|
||||
// Actions
|
||||
purchaseTicketForEvent,
|
||||
handleOpenLightningWallet,
|
||||
resetPaymentState,
|
||||
cleanup,
|
||||
generateTicketQRCode
|
||||
}
|
||||
}
|
||||
123
src/modules/events/composables/useUserTickets.ts
Normal file
123
src/modules/events/composables/useUserTickets.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { computed } from 'vue'
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import type { Ticket } from '@/lib/types/event'
|
||||
import { fetchUserTickets } from '@/lib/api/events'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
interface GroupedTickets {
|
||||
eventId: string
|
||||
tickets: Ticket[]
|
||||
paidCount: number
|
||||
pendingCount: number
|
||||
registeredCount: number
|
||||
}
|
||||
|
||||
export function useUserTickets() {
|
||||
const { isAuthenticated, currentUser } = useAuth()
|
||||
|
||||
const { state: tickets, isLoading, error: asyncError, execute: refresh } = useAsyncState(
|
||||
async () => {
|
||||
if (!isAuthenticated.value || !currentUser.value) {
|
||||
return []
|
||||
}
|
||||
return await fetchUserTickets(currentUser.value.id)
|
||||
},
|
||||
[] as Ticket[],
|
||||
{
|
||||
immediate: false,
|
||||
resetOnExecute: false,
|
||||
}
|
||||
)
|
||||
|
||||
const error = computed(() => {
|
||||
if (asyncError.value) {
|
||||
return {
|
||||
message: asyncError.value instanceof Error
|
||||
? asyncError.value.message
|
||||
: 'An error occurred while fetching tickets'
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const sortedTickets = computed(() => {
|
||||
return [...tickets.value].sort((a, b) =>
|
||||
new Date(b.time).getTime() - new Date(a.time).getTime()
|
||||
)
|
||||
})
|
||||
|
||||
const paidTickets = computed(() => {
|
||||
return sortedTickets.value.filter(ticket => ticket.paid)
|
||||
})
|
||||
|
||||
const pendingTickets = computed(() => {
|
||||
return sortedTickets.value.filter(ticket => !ticket.paid)
|
||||
})
|
||||
|
||||
const registeredTickets = computed(() => {
|
||||
return sortedTickets.value.filter(ticket => ticket.registered)
|
||||
})
|
||||
|
||||
const unregisteredTickets = computed(() => {
|
||||
return sortedTickets.value.filter(ticket => ticket.paid && !ticket.registered)
|
||||
})
|
||||
|
||||
// Group tickets by event
|
||||
const groupedTickets = computed(() => {
|
||||
const groups = new Map<string, GroupedTickets>()
|
||||
|
||||
sortedTickets.value.forEach(ticket => {
|
||||
if (!groups.has(ticket.event)) {
|
||||
groups.set(ticket.event, {
|
||||
eventId: ticket.event,
|
||||
tickets: [],
|
||||
paidCount: 0,
|
||||
pendingCount: 0,
|
||||
registeredCount: 0
|
||||
})
|
||||
}
|
||||
|
||||
const group = groups.get(ticket.event)!
|
||||
group.tickets.push(ticket)
|
||||
|
||||
if (ticket.paid) {
|
||||
group.paidCount++
|
||||
} else {
|
||||
group.pendingCount++
|
||||
}
|
||||
|
||||
if (ticket.registered) {
|
||||
group.registeredCount++
|
||||
}
|
||||
})
|
||||
|
||||
// Convert to array and sort by most recent ticket in each group
|
||||
return Array.from(groups.values()).sort((a, b) => {
|
||||
const aLatest = Math.max(...a.tickets.map(t => new Date(t.time).getTime()))
|
||||
const bLatest = Math.max(...b.tickets.map(t => new Date(t.time).getTime()))
|
||||
return bLatest - aLatest
|
||||
})
|
||||
})
|
||||
|
||||
// Load tickets when authenticated
|
||||
const loadTickets = async () => {
|
||||
if (isAuthenticated.value && currentUser.value) {
|
||||
await refresh()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
tickets: sortedTickets,
|
||||
paidTickets,
|
||||
pendingTickets,
|
||||
registeredTickets,
|
||||
unregisteredTickets,
|
||||
groupedTickets,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
// Actions
|
||||
refresh: loadTickets,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue