const API_BASE = import.meta.env.VITE_API_BASE_URL ?? ""; export type PublicHeroSlide = { id: string; title: string; kicker?: string | null; image: string; targetType?: string | null; targetValue?: string | null; isActive?: boolean; }; export type PublicDestination = { id: string; name: string; image?: string | null; isHot?: boolean; isActive?: boolean; aliases?: { id: string; alias: string }[]; }; export type PublicTheme = { id: string; label: string; image: string; targetType?: string | null; targetValue?: string | null; isActive?: boolean; }; export type PublicCta = { id: string; alt: string; image: string; targetType?: string | null; targetValue?: string | null; isActive?: boolean; }; export type PublicSearchPageKeyword = { id: string; label: string; image?: string | null; isActive?: boolean; sortOrder: number; }; export type PublicSearchPageGroup = { id: string; label: string; items: PublicSearchPageKeyword[]; isActive?: boolean; sortOrder: number; }; export type PublicSearchPageModuleCopy = { title: string; subtitle: string; }; export type PublicSearchPageConfig = { placeholder: string; modules: { seasonalInspiration: PublicSearchPageModuleCopy; preferenceDiscovery: PublicSearchPageModuleCopy; }; popularKeywords: PublicSearchPageKeyword[]; groups: PublicSearchPageGroup[]; /** Transitional fields accepted while an older API response is still cached. */ title?: string; subtitle?: string; }; export type PublicHomeModule = { id: string; label: string; sortOrder: number; isActive?: boolean; templateType?: "explore" | "themes" | "deals" | "hotels" | "vehicles"; content?: { items: Array<{ id: string; title: string; subtitle?: string; description?: string; image?: string | null; targetType?: string | null; targetValue?: string | null; productIds?: string[]; chips?: string[]; isActive: boolean; sortOrder: number; }>; }; }; export type ProductPricingTier = { groupSize: number; adultPrice: number; child6PlusPrice: number; childUnder6Price: number; }; export type PublicProduct = { id: string; sourceId?: number | null; title: string; subtitle?: string | null; destination?: { id: string; name: string } | null; priceAmount?: number | null; priceUnit?: string | null; pricingTiers?: ProductPricingTier[] | null; tags?: string[]; coverImage?: string | null; summary?: string | null; durationDays?: number | null; durationNights?: number | null; departureDates?: string[]; remainingSpots?: number | null; recommendation?: string | null; images?: Array<{ id?: string; url: string; alt?: string | null; sortOrder: number }>; keyFacts?: ProductKeyFact[] | null; contentBlocks?: ProductContentBlock[] | null; detailSections?: ProductDetailSection[] | null; status?: string; sortWeight?: number; }; export type ProductKeyFact = { label: string; value: string }; export type ProductContentBlock = | { type: "title"; text: string } | { type: "image"; url: string; alt?: string | null }; export type ProductDetailBlock = | { type: "text"; text: string } | { type: "image"; url: string; alt?: string | null }; export type ProductDetailSection = { key: string; label: string; title?: string | null; blocks: ProductDetailBlock[]; }; export type PublicSiteConfig = { homeModules?: PublicHomeModule[]; heroSlides?: PublicHeroSlide[]; destinations?: PublicDestination[]; themes?: PublicTheme[]; ctaBanners?: PublicCta[]; destinationRecommendations?: { productIds: string[] }; searchPage?: PublicSearchPageConfig; }; export type PublicUserProfile = { id: string; nickname?: string | null; avatarUrl?: string | null; phoneMasked: string | null; hasPhone: boolean; status: "active" | "disabled" | "anonymized"; source: string; firstSeenAt: string; lastLoginAt: string; createdAt: string; leads?: number; favorites?: number; history?: number; }; export type PublicActivity = { favorites: Array; history: Array; }; export type PublicLeadPayload = { destination?: string; phone: string; contactName?: string; wechat?: string; travelDate?: string; peopleCount?: number; adultCount?: number; childCount?: number; roomType?: string; plan?: string; addOns?: string[]; budgetMin?: number; budgetMax?: number; note?: string; sourcePage?: string; sourceTheme?: string; sourceProductId?: string; }; async function request(path: string, init?: RequestInit) { const response = await fetch(`${API_BASE}${path}`, { ...init, credentials: "include", headers: { "content-type": "application/json", ...init?.headers, }, }); if (!response.ok) { const message = await response.text(); throw new Error(message || `Request failed: ${response.status}`); } return response.json() as Promise; } export async function fetchCurrentUser() { return request("/api/public/users/me"); } export async function exchangeH5Ticket(ticket: string) { return request<{ user: PublicUserProfile }>("/api/public/auth/h5-exchange", { method: "POST", body: JSON.stringify({ ticket }), }); } export async function syncPublicActivity(favoriteProductIds: string[], historyProductIds: string[]) { return request("/api/public/users/me/activity/sync", { method: "POST", body: JSON.stringify({ favoriteProductIds, historyProductIds }), }); } export async function savePublicFavorite(productId: string, saved: boolean) { const path = "/api/public/users/me/favorites/" + encodeURIComponent(productId); return request<{ saved: boolean }>(path, { method: saved ? "POST" : "DELETE" }); } export async function recordPublicHistory(productId: string) { return request<{ saved: boolean }>("/api/public/users/me/history", { method: "POST", body: JSON.stringify({ productId }), }); } export async function clearPublicHistory() { return request<{ cleared: boolean }>("/api/public/users/me/history", { method: "DELETE" }); } export async function logoutPublicSession() { return request<{ loggedOut: boolean }>("/api/public/auth/logout", { method: "POST", body: JSON.stringify({}), }); } export async function fetchPublicContent() { const [siteConfig, products] = await Promise.all([ request("/api/public/site-config"), request<{ items: PublicProduct[] }>("/api/public/products"), ]); return { siteConfig, products: products.items, }; } export async function createPublicLead(payload: PublicLeadPayload) { return request<{ id: string; status: string }>("/api/public/leads", { method: "POST", body: JSON.stringify(payload), }); } export async function createPublicBooking(payload: PublicLeadPayload) { return request<{ id: string; status: string; requestType: "booking" }>("/api/public/bookings", { method: "POST", body: JSON.stringify(payload), }); }