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:
padreug 2025-09-05 00:01:40 +02:00
parent 519a9003d4
commit e40ac91417
46 changed files with 6305 additions and 3264 deletions

View 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,
}
}

View 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
}
}

View 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,
}
}