281 lines
7.0 KiB
TypeScript
281 lines
7.0 KiB
TypeScript
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<PublicProduct & { savedAt: string }>;
|
|
history: Array<PublicProduct & { viewedAt: string; viewCount: number }>;
|
|
};
|
|
|
|
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<T>(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<T>;
|
|
}
|
|
|
|
export async function fetchCurrentUser() {
|
|
return request<PublicUserProfile>("/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<PublicActivity>("/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<PublicSiteConfig>("/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),
|
|
});
|
|
}
|