Compare commits
3 commits
af3c9853c0
...
9b1b56e05d
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b1b56e05d | |||
| 01b871e7fa | |||
| 3047565920 |
3 changed files with 73 additions and 12 deletions
|
|
@ -1,14 +1,43 @@
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useAsyncState } from '@vueuse/core'
|
import { useAsyncState } from '@vueuse/core'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import type { TicketApiService } from '../services/TicketApiService'
|
import type { TicketApiService } from '../services/TicketApiService'
|
||||||
import type { TicketedEvent } from '../types/ticket'
|
import type { TicketedEvent } from '../types/ticket'
|
||||||
|
|
||||||
export function useEvents() {
|
export function useEvents() {
|
||||||
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
|
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
|
||||||
|
const { isAuthenticated, currentUser } = useAuth()
|
||||||
|
|
||||||
|
// When authenticated, also fetch the user's own events (any status)
|
||||||
|
// and merge them into the feed. Otherwise an event that drops to
|
||||||
|
// `proposed` after a non-admin edit disappears from the user's view
|
||||||
|
// entirely — they'd be unable to find it to make a follow-up edit or
|
||||||
|
// monitor its approval status. Public approved events from other
|
||||||
|
// users take precedence on dedup (server is the source of truth for
|
||||||
|
// the public view).
|
||||||
|
const fetchAll = async (): Promise<TicketedEvent[]> => {
|
||||||
|
const publicEvents = (await ticketApi.fetchTicketedEvents()) as TicketedEvent[]
|
||||||
|
|
||||||
|
if (!isAuthenticated.value) return publicEvents
|
||||||
|
|
||||||
|
const invoiceKey = currentUser.value?.wallets?.[0]?.inkey
|
||||||
|
if (!invoiceKey) return publicEvents
|
||||||
|
|
||||||
|
try {
|
||||||
|
const myEvents = (await ticketApi.fetchMyEvents(invoiceKey)) as TicketedEvent[]
|
||||||
|
const seen = new Set(publicEvents.map((e) => e.id))
|
||||||
|
const own = myEvents.filter((e) => !seen.has(e.id))
|
||||||
|
return [...publicEvents, ...own]
|
||||||
|
} catch {
|
||||||
|
// Falling back to just the public feed is acceptable — the user
|
||||||
|
// can still browse, they just won't see their own pending events.
|
||||||
|
return publicEvents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { state: events, isLoading, error: asyncError, execute: refresh } = useAsyncState(
|
const { state: events, isLoading, error: asyncError, execute: refresh } = useAsyncState(
|
||||||
() => ticketApi.fetchTicketedEvents() as Promise<TicketedEvent[]>,
|
fetchAll,
|
||||||
[] as TicketedEvent[],
|
[] as TicketedEvent[],
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,20 @@ export class TicketApiService {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the authenticated user's own events across all their wallets,
|
||||||
|
* regardless of status. Lets the webapp show the user's pending /
|
||||||
|
* rejected events alongside the public approved feed — without this
|
||||||
|
* a user who edits under `auto_approve=false` loses sight of their
|
||||||
|
* own event the moment it drops to `proposed`.
|
||||||
|
*/
|
||||||
|
async fetchMyEvents(invoiceKey: string): Promise<any[]> {
|
||||||
|
return this.request('/events/api/v1/events?all_wallets=true', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'X-API-KEY': invoiceKey },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a ticket purchase (creates a Lightning invoice).
|
* Request a ticket purchase (creates a Lightning invoice).
|
||||||
* Uses POST /tickets/{event_id} with user_id in body (upstream API).
|
* Uses POST /tickets/{event_id} with user_id in body (upstream API).
|
||||||
|
|
@ -187,14 +201,16 @@ export class TicketApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the extension's auto_approve flag. Admin-only endpoint, so
|
* Read the extension's auto_approve flag. Hits the public probe
|
||||||
* non-admin callers see false (the safe default for UI gating).
|
* (invoice-key-gated, available to any wallet holder), so non-admin
|
||||||
|
* users see the real value and the edit-flow copy is accurate.
|
||||||
|
* Degrades to `false` on failure — the safer default for warning UI.
|
||||||
*/
|
*/
|
||||||
async getAutoApprove(adminKey: string): Promise<boolean> {
|
async getAutoApprove(invoiceKey: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const settings = await this.request('/events/api/v1/events/settings', {
|
const settings = await this.request('/events/api/v1/events/settings/public', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'X-API-KEY': adminKey },
|
headers: { 'X-API-KEY': invoiceKey },
|
||||||
})
|
})
|
||||||
return Boolean(settings?.auto_approve)
|
return Boolean(settings?.auto_approve)
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,13 @@ function canEdit(event: TicketedEvent): boolean {
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!isAuthenticated.value) return
|
if (!isAuthenticated.value) return
|
||||||
const adminKey = currentUser.value?.wallets?.[0]?.adminkey
|
const wallet = currentUser.value?.wallets?.[0]
|
||||||
if (!adminKey) return
|
if (!wallet?.inkey) return
|
||||||
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
|
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
|
||||||
isAdmin.value = await ticketApi.isAdmin(adminKey)
|
autoApprove.value = await ticketApi.getAutoApprove(wallet.inkey)
|
||||||
autoApprove.value = await ticketApi.getAutoApprove(adminKey)
|
if (wallet.adminkey) {
|
||||||
|
isAdmin.value = await ticketApi.isAdmin(wallet.adminkey)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function formatDate(dateStr: string | null | undefined) {
|
function formatDate(dateStr: string | null | undefined) {
|
||||||
|
|
@ -159,7 +161,16 @@ function handleEventChanged() {
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<Card v-for="event in upcomingEvents" :key="event.id" class="flex flex-col">
|
<Card v-for="event in upcomingEvents" :key="event.id" class="flex flex-col">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
<CardTitle class="text-foreground">{{ event.name }}</CardTitle>
|
<CardTitle class="text-foreground">{{ event.name }}</CardTitle>
|
||||||
|
<Badge
|
||||||
|
v-if="canEdit(event) && event.status !== 'approved'"
|
||||||
|
:variant="event.status === 'rejected' ? 'destructive' : 'secondary'"
|
||||||
|
class="shrink-0 capitalize"
|
||||||
|
>
|
||||||
|
{{ event.status === 'rejected' ? 'Rejected' : 'Pending review' }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
<CardDescription>{{ event.info }}</CardDescription>
|
<CardDescription>{{ event.info }}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="flex-grow">
|
<CardContent class="flex-grow">
|
||||||
|
|
@ -186,13 +197,18 @@ function handleEventChanged() {
|
||||||
<Button
|
<Button
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
variant="default"
|
variant="default"
|
||||||
:disabled="event.amount_tickets <= event.sold || !isAuthenticated"
|
:disabled="
|
||||||
|
event.status !== 'approved' ||
|
||||||
|
event.amount_tickets <= event.sold ||
|
||||||
|
!isAuthenticated
|
||||||
|
"
|
||||||
@click="handlePurchaseClick(event)"
|
@click="handlePurchaseClick(event)"
|
||||||
>
|
>
|
||||||
<span v-if="!isAuthenticated" class="flex items-center gap-2">
|
<span v-if="!isAuthenticated" class="flex items-center gap-2">
|
||||||
<LogIn class="w-4 h-4" />
|
<LogIn class="w-4 h-4" />
|
||||||
Login to Purchase
|
Login to Purchase
|
||||||
</span>
|
</span>
|
||||||
|
<span v-else-if="event.status !== 'approved'">Not yet available</span>
|
||||||
<span v-else>Buy Ticket</span>
|
<span v-else>Buy Ticket</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue