231 lines
7.1 KiB
TypeScript
231 lines
7.1 KiB
TypeScript
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<number, HomeCategoryPreviewEvent[]> {
|
|
const map = new Map<number, HomeCategoryPreviewEvent[]>()
|
|
|
|
for (const group of feed?.categoryPreviews ?? []) {
|
|
map.set(group.categoryId, group.items)
|
|
}
|
|
|
|
return map
|
|
}
|
|
|
|
export async function fetchDiscoveryCities(): Promise<City[]> {
|
|
const response = await eventsApi.discoveryBootstrapControllerListCities()
|
|
const payload = unwrapApiPayload<City[]>(response.data)
|
|
|
|
return payload ?? []
|
|
}
|
|
|
|
export async function fetchDiscoveryCategories(): Promise<DiscoveryCategory[]> {
|
|
const response = await eventsApi.discoveryBootstrapControllerListCategories()
|
|
const payload = unwrapApiPayload<DiscoveryCategory[]>(response.data)
|
|
|
|
return payload ?? []
|
|
}
|
|
|
|
export async function fetchHomeFeed(query: HomeFeedQuery = {}, options?: ServiceCallOptions): Promise<ServiceResult<HomeFeed>> {
|
|
try {
|
|
const response = await eventsApi.discoveryBootstrapControllerGetHomeFeed({
|
|
cityId: query.cityId,
|
|
})
|
|
const payload = unwrapApiPayload<HomeFeed>(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<DiscoveryBootstrap> {
|
|
const response = await axiosInstance.get('discovery/bootstrap')
|
|
const payload = unwrapApiPayload<DiscoveryBootstrap>(response.data)
|
|
|
|
return payload ?? { categories: [], cities: [] }
|
|
}
|
|
|
|
/** Full category list via event-categories module (admin-adjacent surfaces). */
|
|
export async function fetchEventCategoriesList(options?: ServiceCallOptions): Promise<ServiceResult<DiscoveryCategory[]>> {
|
|
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<ServiceResult<DiscoveryListResult>> {
|
|
try {
|
|
const params: {
|
|
page: number
|
|
pageSize: number
|
|
sort?: string
|
|
filters: Record<string, string>
|
|
} = {
|
|
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)
|
|
}
|
|
}
|