Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 5x 5x 2x 2x 2x 2x 2x 2x 4x 2x 2x 2x 1x 2x 2x 2x 2x 2x 2x 5x 2x 5x 5x 5x 5x 3x 3x 2x 5x | import { useCallback, useEffect, useMemo, useState } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { ApiError } from '@/services/api';
import { getEventDetail } from '@/services/eventService';
import { listMyTickets } from '@/services/ticketService';
import type { EventDetail } from '@/models/event';
import type { TicketListItem } from '@/models/ticket';
import i18n from '@/i18n';
export interface EventActionsViewModel {
event: EventDetail | null;
ticket: TicketListItem | null;
isLoading: boolean;
errorMessage: string | null;
primaryActionLabel: string | null;
canOpenTicket: boolean;
canScanTicket: boolean;
canEditEvent: boolean;
reload: () => Promise<void>;
}
function getLoadErrorMessage(error: unknown): string {
Iif (error instanceof ApiError && error.status === 401) {
return i18n.t('tickets.actions.errors.loginRequired');
}
Iif (error instanceof ApiError) {
return error.message;
}
return i18n.t('tickets.actions.errors.loadFailed');
}
export function useEventActionsViewModel(eventId: string): EventActionsViewModel {
const { token } = useAuth();
const [event, setEvent] = useState<EventDetail | null>(null);
const [ticket, setTicket] = useState<TicketListItem | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const reload = useCallback(async () => {
Iif (!token) {
setEvent(null);
setTicket(null);
setErrorMessage(i18n.t('tickets.actions.errors.loginRequired'));
setIsLoading(false);
return;
}
setIsLoading(true);
setErrorMessage(null);
try {
const eventDetail = await getEventDetail(eventId, token);
setEvent(eventDetail);
const findTicket = (items: TicketListItem[]) => items.find((item) => item.event.id === eventId) ?? null;
let ticketsResponse = await listMyTickets(token).catch(() => ({ items: [] }));
let foundTicket = findTicket(ticketsResponse.items);
// RETRY LOGIC: If joined a PUBLIC event but ticket is not yet in the list,
// the server might still be processing the async ticket creation.
// We retry up to 2 times with a 1s delay (3 attempts total).
if (!foundTicket && eventDetail.viewer_context.participation_status === 'JOINED' && eventDetail.privacy_level === 'PUBLIC') {
for (let i = 0; i < 2; i++) {
await new Promise(r => setTimeout(r, 1000));
ticketsResponse = await listMyTickets(token).catch(() => ({ items: [] }));
foundTicket = findTicket(ticketsResponse.items);
if (foundTicket) break;
}
}
setTicket(foundTicket);
} catch (error) {
setEvent(null);
setTicket(null);
setErrorMessage(getLoadErrorMessage(error));
} finally {
setIsLoading(false);
}
}, [eventId, token]);
useEffect(() => {
void reload();
}, [reload]);
const canScanTicket = Boolean(event?.viewer_context.is_host);
const canOpenTicket = !canScanTicket && ticket != null;
const canEditEvent = Boolean(
event?.viewer_context.is_host &&
event.status === 'ACTIVE' &&
new Date(event.start_time).getTime() > Date.now(),
);
const primaryActionLabel = useMemo(() => {
Iif (canScanTicket) return i18n.t('tickets.actions.scanTicket');
if (canOpenTicket) return i18n.t('tickets.actions.viewTicket');
return null;
}, [canOpenTicket, canScanTicket]);
return {
event,
ticket,
isLoading,
errorMessage,
primaryActionLabel,
canOpenTicket,
canScanTicket,
canEditEvent,
reload,
};
}
|