import type { DiscoveryCategorySummaryDto, HomeCategoryPreviewEventDto, HomeFeedResponseDto, HomePopularEventDto, } from '@/api/generated/models' import { getEvents } from '@/api/generated/events/events' import { DISCOVERY_PAGE_SIZE } from '@/constants/discovery' import axiosInstance from '@/config/axios' import type { City } from '@/services/geography' import { errorResult, handleServiceError, type ServiceCallOptions, type ServiceResult, shouldBubbleErrorToParent, successResult, } from '@/services/errorHandler' import { getEventCategories } from '@/api/generated/event-categories/event-categories' import { unwrapApiPayload } from '@/helpers/listResponse' import type { EventStatus } from '@/types/status' /** Lightweight host name on public discovery cards. */ export interface DiscoveryEventOrganizer { firstName: string | null lastName: string | null } /** * Public discovery event shape (GET /events). * Hand-written so type-aware ESLint resolves it while `api/generated` stays eslint-ignored. */ export interface DiscoveryEvent { id: string organizerId: string categoryId: number clonedFromEventId: string | null title: string slug: string shortDescription: string | null description: string | null startsAt: string endsAt: string provinceId: number cityId: number address: string | null lat: number | null lng: number | null isFree: boolean price: number capacity: number bookedCount: number reservedCapacity: number cancellationFeePercent: number cancellationFeePercent12To24Hours: number cancellationFeePercentMoreThan24Hours: number commissionPercent: number | null effectiveCommissionPercent: number status: EventStatus isFeatured: boolean publishedAt: string | null adminApprovedAt: string | null rejectionReason: string | null avgRating?: number | null reviewsCount: number createdAt: string updatedAt: string posterUrl: string | null squarePosterUrl: string | null organizer: DiscoveryEventOrganizer } export type DiscoveryCategory = DiscoveryCategorySummaryDto export type HomePopularEvent = HomePopularEventDto export type HomeCategoryPreviewEvent = HomeCategoryPreviewEventDto export type HomeFeed = HomeFeedResponseDto export interface DiscoveryFilters { categoryIds?: number[] cityId?: number provinceId?: number startsAtFrom?: string startsAtTo?: string } export interface DiscoveryListResult { items: DiscoveryEvent[] totalItemsCount: number } export interface DiscoveryPagination { page?: number pageSize?: number sort?: string } export interface HomeFeedQuery { cityId?: number } export interface DiscoveryBootstrap { categories: DiscoveryCategory[] cities: City[] } const eventsApi = getEvents() const eventCategoriesApi = getEventCategories() // Cities and categories are shared public reference data, but are still // admin-managed. A finite window lets long-lived tabs see taxonomy changes // after focus/reconnect instead of keeping them stale until a deploy. const REFERENCE_STALE_MS = 5 * 60_000 const HOME_FEED_STALE_MS = 60_000 export const discoveryReferenceStaleTime = REFERENCE_STALE_MS export const discoveryHomeFeedStaleTime = HOME_FEED_STALE_MS export function homeCategoryPreviewMap(feed: HomeFeed | undefined): Map { const map = new Map() for (const group of feed?.categoryPreviews ?? []) { map.set(group.categoryId, group.items) } return map } export async function fetchDiscoveryCities(): Promise { const response = await eventsApi.discoveryBootstrapControllerListCities() const payload = unwrapApiPayload(response.data) return payload ?? [] } export async function fetchDiscoveryCategories(): Promise { const response = await eventsApi.discoveryBootstrapControllerListCategories() const payload = unwrapApiPayload(response.data) return payload ?? [] } export async function fetchHomeFeed(query: HomeFeedQuery = {}, options?: ServiceCallOptions): Promise> { try { const response = await eventsApi.discoveryBootstrapControllerGetHomeFeed({ cityId: query.cityId, }) const payload = unwrapApiPayload(response.data) if (!payload) { throw new Error('Home feed fetch returned no data') } return successResult(payload) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } } /** @deprecated Prefer `fetchDiscoveryCities` + `fetchDiscoveryCategories`. */ export async function fetchDiscoveryBootstrap(): Promise { const response = await axiosInstance.get('discovery/bootstrap') const payload = unwrapApiPayload(response.data) return payload ?? { categories: [], cities: [] } } /** Full category list via event-categories module (admin-adjacent surfaces). */ export async function fetchEventCategoriesList(options?: ServiceCallOptions): Promise> { try { const response = await eventCategoriesApi.eventCategoriesControllerList() const payload = unwrapApiPayload(response.data) return successResult(Array.isArray(payload) ? (payload as DiscoveryCategory[]) : []) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } } export async function fetchDiscoveryEvents( filters: DiscoveryFilters, paginationOptions: DiscoveryPagination = {}, options?: ServiceCallOptions ): Promise> { try { const params: { page: number pageSize: number sort?: string filters: Record } = { page: paginationOptions.page ?? 1, pageSize: paginationOptions.pageSize ?? DISCOVERY_PAGE_SIZE, filters: {}, } if (paginationOptions.sort) params.sort = paginationOptions.sort if (filters.categoryIds?.length) params.filters.categoryId = filters.categoryIds.join(',') if (filters.cityId !== undefined) params.filters.cityId = String(filters.cityId) if (filters.provinceId !== undefined) params.filters.provinceId = String(filters.provinceId) if (filters.startsAtFrom) params.filters.startsAtFrom = filters.startsAtFrom if (filters.startsAtTo) params.filters.startsAtTo = filters.startsAtTo const response = await eventsApi.eventsControllerDiscovery(params) const body = response.data as { data?: { items?: DiscoveryEvent[]; response?: { totalItemsCount?: number } } } const data = body.data ?? {} const items = Array.isArray(data.items) ? data.items : [] const totalItemsCount = data.response?.totalItemsCount ?? items.length return successResult({ items, totalItemsCount }) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } }