@questpackages/tours 0.1.0-test.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +122 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +219 -0
- package/dist/cli/templates.d.ts +12 -0
- package/dist/cli/templates.js +100 -0
- package/dist/next/index.d.ts +4 -0
- package/dist/next/index.js +4 -0
- package/dist/next/metadata.d.ts +26 -0
- package/dist/next/metadata.js +58 -0
- package/dist/next/revalidate-handler.d.ts +18 -0
- package/dist/next/revalidate-handler.js +42 -0
- package/dist/next/signature.d.ts +12 -0
- package/dist/next/signature.js +26 -0
- package/dist/react/booking-steps.d.ts +22 -0
- package/dist/react/booking-steps.js +23 -0
- package/dist/react/booking-wizard.d.ts +12 -0
- package/dist/react/booking-wizard.js +68 -0
- package/dist/react/contact-dialog.d.ts +15 -0
- package/dist/react/contact-dialog.js +45 -0
- package/dist/react/contact-panel.d.ts +5 -0
- package/dist/react/contact-panel.js +15 -0
- package/dist/react/contact-shared.d.ts +55 -0
- package/dist/react/contact-shared.js +60 -0
- package/dist/react/countries.d.ts +4 -0
- package/dist/react/countries.js +6 -0
- package/dist/react/index.d.ts +12 -0
- package/dist/react/index.js +11 -0
- package/dist/react/inquiry-wizard.d.ts +5 -0
- package/dist/react/inquiry-wizard.js +43 -0
- package/dist/react/party-size-rules.d.ts +18 -0
- package/dist/react/party-size-rules.js +52 -0
- package/dist/react/party-size.d.ts +27 -0
- package/dist/react/party-size.js +81 -0
- package/dist/react/sections.d.ts +10 -0
- package/dist/react/sections.js +58 -0
- package/dist/react/shared.d.ts +50 -0
- package/dist/react/shared.js +39 -0
- package/dist/react/tour-card.d.ts +30 -0
- package/dist/react/tour-card.js +22 -0
- package/dist/react/tour-detail.d.ts +26 -0
- package/dist/react/tour-detail.js +55 -0
- package/dist/react/tour-gallery.d.ts +4 -0
- package/dist/react/tour-gallery.js +35 -0
- package/dist/react/tour-json-ld.d.ts +7 -0
- package/dist/react/tour-json-ld.js +13 -0
- package/dist/schema/index.d.ts +805 -0
- package/dist/schema/index.js +249 -0
- package/dist/server/client.d.ts +43 -0
- package/dist/server/client.js +85 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +2 -0
- package/dist/styles.css +1395 -0
- package/package.json +67 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Public data contract between Quest and operator websites (API v1).
|
|
4
|
+
*
|
|
5
|
+
* Quest validates every `/api/public/v1` response against these schemas, and the SDK
|
|
6
|
+
* parses every response with them. A section that Quest does not model has no field
|
|
7
|
+
* here, so an operator site cannot render it from Quest data.
|
|
8
|
+
*
|
|
9
|
+
* Additive changes (new optional fields) are allowed in v1. Removing or renaming a
|
|
10
|
+
* field requires API v2.
|
|
11
|
+
*/
|
|
12
|
+
export const QUEST_TOURS_API_VERSION = 1;
|
|
13
|
+
export const tourImageSchema = z.object({
|
|
14
|
+
url: z.string().url(),
|
|
15
|
+
alt: z.string(),
|
|
16
|
+
srcSet: z.string().nullable(),
|
|
17
|
+
width: z.number().int().positive().nullable(),
|
|
18
|
+
height: z.number().int().positive().nullable(),
|
|
19
|
+
});
|
|
20
|
+
export const tourTagSchema = z.object({
|
|
21
|
+
slug: z.string().min(1),
|
|
22
|
+
label: z.string().min(1),
|
|
23
|
+
});
|
|
24
|
+
/** A price as Quest displays it, plus the parsed amount for structured data and sorting. */
|
|
25
|
+
export const tourMoneySchema = z.object({
|
|
26
|
+
formatted: z.string().min(1),
|
|
27
|
+
amount: z.number().nonnegative().nullable(),
|
|
28
|
+
currency: z.string().min(3).max(8),
|
|
29
|
+
});
|
|
30
|
+
export const tourCardSchema = z.object({
|
|
31
|
+
slug: z.string().min(1),
|
|
32
|
+
title: z.string().min(1),
|
|
33
|
+
summary: z.string().nullable(),
|
|
34
|
+
durationDays: z.number().int().positive(),
|
|
35
|
+
durationLabel: z.string(),
|
|
36
|
+
startDestination: z.string().nullable(),
|
|
37
|
+
endDestination: z.string().nullable(),
|
|
38
|
+
/** Destinations visited, in itinerary order (e.g. for destination pages on the operator site). */
|
|
39
|
+
destinations: z.array(z.string()),
|
|
40
|
+
heroImage: tourImageSchema.nullable(),
|
|
41
|
+
highlights: z.array(z.string()),
|
|
42
|
+
includedPreview: z.array(z.string()),
|
|
43
|
+
tags: z.array(tourTagSchema),
|
|
44
|
+
featured: z.boolean(),
|
|
45
|
+
price: z.object({
|
|
46
|
+
total: tourMoneySchema.nullable(),
|
|
47
|
+
fromPerPerson: tourMoneySchema.nullable(),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
export const tourActivitySchema = z.object({
|
|
51
|
+
time: z.string().nullable(),
|
|
52
|
+
name: z.string().min(1),
|
|
53
|
+
location: z.string().nullable(),
|
|
54
|
+
notes: z.string().nullable(),
|
|
55
|
+
});
|
|
56
|
+
export const tourDaySchema = z.object({
|
|
57
|
+
dayLabel: z.string(),
|
|
58
|
+
dateLabel: z.string().nullable(),
|
|
59
|
+
destination: z.string(),
|
|
60
|
+
title: z.string(),
|
|
61
|
+
description: z.string(),
|
|
62
|
+
image: tourImageSchema.nullable(),
|
|
63
|
+
mealPlan: z.string().nullable(),
|
|
64
|
+
accommodation: z
|
|
65
|
+
.object({
|
|
66
|
+
label: z.string(),
|
|
67
|
+
name: z.string().min(1),
|
|
68
|
+
detail: z.string(),
|
|
69
|
+
image: tourImageSchema.nullable(),
|
|
70
|
+
})
|
|
71
|
+
.nullable(),
|
|
72
|
+
activityGroups: z.array(z.object({
|
|
73
|
+
title: z.string(),
|
|
74
|
+
activities: z.array(tourActivitySchema),
|
|
75
|
+
})),
|
|
76
|
+
activities: z.array(tourActivitySchema),
|
|
77
|
+
gallery: z.array(tourImageSchema),
|
|
78
|
+
});
|
|
79
|
+
export const tourLodgingSchema = z.object({
|
|
80
|
+
name: z.string().min(1),
|
|
81
|
+
label: z.string(),
|
|
82
|
+
detail: z.string(),
|
|
83
|
+
image: tourImageSchema.nullable(),
|
|
84
|
+
nights: z.array(z.string()),
|
|
85
|
+
mealPlan: z.string().nullable(),
|
|
86
|
+
});
|
|
87
|
+
export const tourVehicleSchema = z.object({
|
|
88
|
+
name: z.string().min(1),
|
|
89
|
+
description: z.string(),
|
|
90
|
+
image: tourImageSchema.nullable(),
|
|
91
|
+
features: z.array(z.string()),
|
|
92
|
+
maxPassengers: z.number().int().positive().nullable(),
|
|
93
|
+
});
|
|
94
|
+
export const tourPartyPriceOptionSchema = z.object({
|
|
95
|
+
adults: z.number().int().positive(),
|
|
96
|
+
children: z.number().int().nonnegative(),
|
|
97
|
+
total: z.string(),
|
|
98
|
+
perPerson: z.string(),
|
|
99
|
+
vehicleCapacityLabel: z.string().nullable(),
|
|
100
|
+
});
|
|
101
|
+
export const tourPricingSchema = z.object({
|
|
102
|
+
currency: z.string().min(3).max(8),
|
|
103
|
+
total: tourMoneySchema.nullable(),
|
|
104
|
+
perPerson: tourMoneySchema.nullable(),
|
|
105
|
+
fromPerPerson: tourMoneySchema.nullable(),
|
|
106
|
+
/** Empty when the operator hides the detailed price breakdown in Quest. */
|
|
107
|
+
lineItems: z.array(z.object({
|
|
108
|
+
name: z.string(),
|
|
109
|
+
qty: z.string(),
|
|
110
|
+
unitPrice: z.string(),
|
|
111
|
+
total: z.string(),
|
|
112
|
+
})),
|
|
113
|
+
/** Price per party size. Null when the package has no party pricing matrix. */
|
|
114
|
+
partySize: z
|
|
115
|
+
.object({
|
|
116
|
+
minTravellers: z.number().int().positive(),
|
|
117
|
+
maxTravellers: z.number().int().positive(),
|
|
118
|
+
baselineAdults: z.number().int().positive(),
|
|
119
|
+
childrenAllowed: z.boolean(),
|
|
120
|
+
options: z.array(tourPartyPriceOptionSchema),
|
|
121
|
+
})
|
|
122
|
+
.nullable(),
|
|
123
|
+
});
|
|
124
|
+
export const tourOperatorSchema = z.object({
|
|
125
|
+
name: z.string().min(1),
|
|
126
|
+
logoUrl: z.string().url().nullable(),
|
|
127
|
+
website: z.string().nullable(),
|
|
128
|
+
phone: z.string().nullable(),
|
|
129
|
+
address: z.string().nullable(),
|
|
130
|
+
});
|
|
131
|
+
export const tourEnquirySchema = z.object({
|
|
132
|
+
/** Absolute URL the visitor's browser POSTs the inquiry form to. */
|
|
133
|
+
endpoint: z.string().url(),
|
|
134
|
+
/** Absolute URL the visitor's browser POSTs the booking request form to. */
|
|
135
|
+
bookingEndpoint: z.string().url(),
|
|
136
|
+
minTravellers: z.number().int().positive(),
|
|
137
|
+
maxTravellers: z.number().int().positive(),
|
|
138
|
+
childrenAllowed: z.boolean(),
|
|
139
|
+
});
|
|
140
|
+
export const tourSchema = tourCardSchema.extend({
|
|
141
|
+
overview: z.array(z.string()),
|
|
142
|
+
gallery: z.array(tourImageSchema),
|
|
143
|
+
days: z.array(tourDaySchema),
|
|
144
|
+
lodging: z.array(tourLodgingSchema),
|
|
145
|
+
vehicle: tourVehicleSchema.nullable(),
|
|
146
|
+
inclusions: z.array(z.string()),
|
|
147
|
+
exclusions: z.array(z.string()),
|
|
148
|
+
paymentTerms: z.array(z.string()),
|
|
149
|
+
paymentNote: z.string().nullable(),
|
|
150
|
+
availability: z.object({
|
|
151
|
+
unlimited: z.boolean(),
|
|
152
|
+
startDate: z.string().nullable(),
|
|
153
|
+
endDate: z.string().nullable(),
|
|
154
|
+
}),
|
|
155
|
+
pricing: tourPricingSchema,
|
|
156
|
+
operator: tourOperatorSchema,
|
|
157
|
+
enquiry: tourEnquirySchema,
|
|
158
|
+
related: z.array(tourCardSchema),
|
|
159
|
+
});
|
|
160
|
+
/** SEO built by Quest for the operator page URL passed as `?url=`. Follows the Quest SEO guide. */
|
|
161
|
+
export const tourSeoSchema = z.object({
|
|
162
|
+
title: z.string(),
|
|
163
|
+
description: z.string(),
|
|
164
|
+
canonicalUrl: z.string().url(),
|
|
165
|
+
ogImage: z.string().nullable(),
|
|
166
|
+
heroPreloadUrl: z.string().nullable(),
|
|
167
|
+
jsonLd: z.array(z.record(z.string(), z.unknown())),
|
|
168
|
+
});
|
|
169
|
+
export const tourListResponseSchema = z.object({
|
|
170
|
+
apiVersion: z.literal(QUEST_TOURS_API_VERSION),
|
|
171
|
+
data: z.object({
|
|
172
|
+
tours: z.array(tourCardSchema),
|
|
173
|
+
nextCursor: z.string().nullable(),
|
|
174
|
+
totalCount: z.number().int().nonnegative(),
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
export const tourDetailResponseSchema = z.object({
|
|
178
|
+
apiVersion: z.literal(QUEST_TOURS_API_VERSION),
|
|
179
|
+
data: z.object({
|
|
180
|
+
tour: tourSchema,
|
|
181
|
+
seo: tourSeoSchema.nullable(),
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
export const tourTagsResponseSchema = z.object({
|
|
185
|
+
apiVersion: z.literal(QUEST_TOURS_API_VERSION),
|
|
186
|
+
data: z.object({
|
|
187
|
+
tags: z.array(tourTagSchema.extend({ count: z.number().int().nonnegative() })),
|
|
188
|
+
}),
|
|
189
|
+
});
|
|
190
|
+
export const questApiErrorSchema = z.object({
|
|
191
|
+
apiVersion: z.literal(QUEST_TOURS_API_VERSION),
|
|
192
|
+
error: z.object({
|
|
193
|
+
code: z.enum(["not_found", "unavailable", "invalid_request", "internal"]),
|
|
194
|
+
message: z.string(),
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
/** Fields the enquiry endpoint accepts. Mirrors Quest's inquiry validation. */
|
|
198
|
+
export const tourEnquiryInputSchema = z.object({
|
|
199
|
+
name: z.string().trim().min(2, "Please enter your name").max(120),
|
|
200
|
+
email: z.string().trim().email("Please enter a valid email").max(200),
|
|
201
|
+
phone: z.string().trim().min(6, "Enter a phone or WhatsApp number").max(50),
|
|
202
|
+
countryCode: z.string().trim().length(2, "Select a country"),
|
|
203
|
+
numberOfAdults: z.number().int().min(1).max(30).optional(),
|
|
204
|
+
numberOfChildren: z.number().int().min(0).max(20).optional(),
|
|
205
|
+
travelDateMode: z.enum(["flexible", "specific_month", "specific_date"]),
|
|
206
|
+
preferredStartDate: z.string().trim().optional(),
|
|
207
|
+
preferredMonth: z.string().trim().regex(/^\d{4}-\d{2}$/).optional(),
|
|
208
|
+
message: z.string().trim().max(2000).optional(),
|
|
209
|
+
indicativeTotal: z.string().trim().max(80).optional(),
|
|
210
|
+
indicativePerPerson: z.string().trim().max(80).optional(),
|
|
211
|
+
indicativeCurrency: z.string().trim().max(8).optional(),
|
|
212
|
+
});
|
|
213
|
+
/** Fields the booking request endpoint accepts. Mirrors Quest's booking validation. */
|
|
214
|
+
export const tourBookingInputSchema = z.object({
|
|
215
|
+
name: z.string().trim().min(2).max(120),
|
|
216
|
+
email: z.string().trim().email().max(200),
|
|
217
|
+
phone: z.string().trim().min(6).max(50),
|
|
218
|
+
nationalityCode: z.string().trim().length(2),
|
|
219
|
+
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
220
|
+
numberOfAdults: z.number().int().min(1).max(30),
|
|
221
|
+
numberOfChildren: z.number().int().min(0).max(20),
|
|
222
|
+
childAges: z.array(z.number().int().min(0).max(17)).max(20),
|
|
223
|
+
accommodationTier: z.enum(["budget", "mid_range", "luxury"]).optional(),
|
|
224
|
+
singleSupplement: z.boolean(),
|
|
225
|
+
specialRequirements: z.string().trim().max(2000).optional(),
|
|
226
|
+
termsAccepted: z.literal(true),
|
|
227
|
+
indicativeTotal: z.string().trim().max(80).optional(),
|
|
228
|
+
indicativePerPerson: z.string().trim().max(80).optional(),
|
|
229
|
+
indicativeCurrency: z.string().trim().max(8).optional(),
|
|
230
|
+
});
|
|
231
|
+
/** Successful inquiry or booking submission, as returned by Quest. */
|
|
232
|
+
export const tourContactSuccessSchema = z.object({
|
|
233
|
+
ok: z.literal(true),
|
|
234
|
+
referenceNumber: z.string(),
|
|
235
|
+
summary: z.object({
|
|
236
|
+
packageTitle: z.string(),
|
|
237
|
+
travelDatesLabel: z.string(),
|
|
238
|
+
adults: z.number().optional(),
|
|
239
|
+
children: z.number().optional(),
|
|
240
|
+
contactName: z.string(),
|
|
241
|
+
}),
|
|
242
|
+
});
|
|
243
|
+
/** Body of the revalidate webhook Quest sends to the operator site. */
|
|
244
|
+
export const toursChangedEventSchema = z.object({
|
|
245
|
+
event: z.literal("tours.changed"),
|
|
246
|
+
siteToken: z.string().min(1),
|
|
247
|
+
slugs: z.array(z.string()),
|
|
248
|
+
sentAt: z.string(),
|
|
249
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Tour, type TourCard, type TourSeo, type TourTag } from "../schema/index.js";
|
|
2
|
+
/** Cache tag on every Quest fetch. The revalidate webhook handler expires it. */
|
|
3
|
+
export declare const QUEST_TOURS_CACHE_TAG = "quest-tours";
|
|
4
|
+
export type QuestToursConfig = {
|
|
5
|
+
/** Public site token from Quest (Website embeds). Defaults to `process.env.QUEST_SITE_TOKEN`. */
|
|
6
|
+
siteToken?: string;
|
|
7
|
+
/** Quest app origin, e.g. `https://app.questheaven.com`. Defaults to `process.env.QUEST_ORIGIN`. */
|
|
8
|
+
questOrigin?: string;
|
|
9
|
+
/** Fallback refresh interval when the webhook is not set up. Default 300 seconds. */
|
|
10
|
+
revalidateSeconds?: number;
|
|
11
|
+
/** Custom fetch (tests). */
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
};
|
|
14
|
+
export declare class QuestToursError extends Error {
|
|
15
|
+
readonly code: string;
|
|
16
|
+
readonly status: number;
|
|
17
|
+
constructor(message: string, code: string, status: number);
|
|
18
|
+
}
|
|
19
|
+
export type GetToursOptions = {
|
|
20
|
+
/** Tag slug, e.g. `gorilla-trekking`. */
|
|
21
|
+
tag?: string;
|
|
22
|
+
/** Only tours marked as featured in Quest. */
|
|
23
|
+
featured?: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type GetTourOptions = {
|
|
26
|
+
/**
|
|
27
|
+
* Absolute URL of the page that shows this tour on your site. When set, Quest returns
|
|
28
|
+
* SEO metadata and JSON-LD built for that URL.
|
|
29
|
+
*/
|
|
30
|
+
url?: string;
|
|
31
|
+
};
|
|
32
|
+
export type TourWithSeo = {
|
|
33
|
+
tour: Tour;
|
|
34
|
+
seo: TourSeo | null;
|
|
35
|
+
};
|
|
36
|
+
export type QuestToursClient = {
|
|
37
|
+
getTours(options?: GetToursOptions): Promise<TourCard[]>;
|
|
38
|
+
getTour(slug: string, options?: GetTourOptions): Promise<TourWithSeo | null>;
|
|
39
|
+
getTags(): Promise<(TourTag & {
|
|
40
|
+
count: number;
|
|
41
|
+
})[]>;
|
|
42
|
+
};
|
|
43
|
+
export declare function createQuestTours(config?: QuestToursConfig): QuestToursClient;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { questApiErrorSchema, tourDetailResponseSchema, tourListResponseSchema, tourTagsResponseSchema, } from "../schema/index.js";
|
|
2
|
+
/** Cache tag on every Quest fetch. The revalidate webhook handler expires it. */
|
|
3
|
+
export const QUEST_TOURS_CACHE_TAG = "quest-tours";
|
|
4
|
+
const DEFAULT_REVALIDATE_SECONDS = 300;
|
|
5
|
+
const LIST_PAGE_SIZE = 24;
|
|
6
|
+
const MAX_LIST_PAGES = 20;
|
|
7
|
+
export class QuestToursError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
status;
|
|
10
|
+
constructor(message, code, status) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "QuestToursError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.status = status;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function requireValue(value, envName) {
|
|
18
|
+
const trimmed = value?.trim();
|
|
19
|
+
if (!trimmed) {
|
|
20
|
+
throw new QuestToursError(`Missing ${envName}. Add it to your environment (run \`npx @questpackages/tours init\`).`, "config", 0);
|
|
21
|
+
}
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
export function createQuestTours(config = {}) {
|
|
25
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
26
|
+
const revalidate = config.revalidateSeconds ?? DEFAULT_REVALIDATE_SECONDS;
|
|
27
|
+
function baseUrl() {
|
|
28
|
+
const origin = requireValue(config.questOrigin ?? process.env.QUEST_ORIGIN, "QUEST_ORIGIN");
|
|
29
|
+
const siteToken = requireValue(config.siteToken ?? process.env.QUEST_SITE_TOKEN, "QUEST_SITE_TOKEN");
|
|
30
|
+
return `${origin.replace(/\/+$/, "")}/api/public/v1/sites/${encodeURIComponent(siteToken)}`;
|
|
31
|
+
}
|
|
32
|
+
async function request(path) {
|
|
33
|
+
const response = await fetchImpl(`${baseUrl()}${path}`, {
|
|
34
|
+
headers: { Accept: "application/json" },
|
|
35
|
+
next: { tags: [QUEST_TOURS_CACHE_TAG], revalidate },
|
|
36
|
+
});
|
|
37
|
+
const body = await response.json().catch(() => null);
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const parsed = questApiErrorSchema.safeParse(body);
|
|
40
|
+
throw new QuestToursError(parsed.success ? parsed.data.error.message : `Quest responded with HTTP ${response.status}`, parsed.success ? parsed.data.error.code : "http", response.status);
|
|
41
|
+
}
|
|
42
|
+
return { status: response.status, body };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
async getTours(options = {}) {
|
|
46
|
+
const tours = [];
|
|
47
|
+
let cursor = null;
|
|
48
|
+
for (let page = 0; page < MAX_LIST_PAGES; page += 1) {
|
|
49
|
+
const params = new URLSearchParams({ limit: String(LIST_PAGE_SIZE) });
|
|
50
|
+
if (options.tag)
|
|
51
|
+
params.set("tag", options.tag);
|
|
52
|
+
if (options.featured)
|
|
53
|
+
params.set("featured", "1");
|
|
54
|
+
if (cursor)
|
|
55
|
+
params.set("cursor", cursor);
|
|
56
|
+
const { body } = await request(`/tours?${params.toString()}`);
|
|
57
|
+
const { data } = tourListResponseSchema.parse(body);
|
|
58
|
+
tours.push(...data.tours);
|
|
59
|
+
cursor = data.nextCursor;
|
|
60
|
+
if (!cursor)
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
return tours;
|
|
64
|
+
},
|
|
65
|
+
async getTour(slug, options = {}) {
|
|
66
|
+
const params = new URLSearchParams();
|
|
67
|
+
if (options.url)
|
|
68
|
+
params.set("url", options.url);
|
|
69
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
70
|
+
try {
|
|
71
|
+
const { body } = await request(`/tours/${encodeURIComponent(slug)}${query}`);
|
|
72
|
+
return tourDetailResponseSchema.parse(body).data;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof QuestToursError && error.status === 404)
|
|
76
|
+
return null;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
async getTags() {
|
|
81
|
+
const { body } = await request("/tags");
|
|
82
|
+
return tourTagsResponseSchema.parse(body).data.tags;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|