@travories/frontend-sdk 0.1.4 → 0.1.5
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/README.md +334 -169
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@travories/frontend-sdk)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
|
-
> Official Travories SDK. Drop the full **package detail page** and **
|
|
8
|
+
> Official Travories SDK. Drop **landing carousels**, the full **package detail page**, the **agency profile page**, and a real **booking flow** into any React app — or just use the API client to fetch data on your own terms.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
npm install @travories/frontend-sdk
|
|
@@ -22,38 +22,30 @@ export default function Page({ slug }: { slug: string }) {
|
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
That's a complete, production-grade package detail page: gallery, itinerary with map
|
|
25
|
+
That's a complete, production-grade package detail page: gallery, itinerary with map and per-day routes, booking sidebar, host card. ~100 KB gzipped, no Tailwind required in your app.
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
##
|
|
30
|
-
|
|
31
|
-
- **Framework-agnostic API client** — fetch packages, hosts, routes, home sections. Works in Node, browser, Next.js server components, edge runtimes.
|
|
32
|
-
- **Drop-in pages** — `<PackageDetailView />` for detail, `<HomeSections />` for landing. One prop and you're done.
|
|
33
|
-
- **Composable layouts** — `<HomeSectionsProvider>` + `<PackagesSection>` lets you interleave SDK sections with your own content (FAQs, banners, etc.), all sharing one fetch.
|
|
34
|
-
- **Real maps** — Leaflet-powered map with per-day GeoJSON routes flown to on day change.
|
|
35
|
-
- **Bundled styling** — Tailwind precompiled and scoped to `.tvr-package-detail`. No Tailwind required in your app, no class conflicts with your own.
|
|
36
|
-
- **Full TypeScript types** — every prop, every API response shape, ergonomic union types.
|
|
37
|
-
- **ESM + CJS + sourcemaps** — works with any bundler, any Node version ≥ 18.
|
|
38
|
-
- **SSR-first** — pass `initialBundle` / `initialSections` to skip client fetches and render on the server.
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## Contents
|
|
29
|
+
## Table of contents
|
|
43
30
|
|
|
44
31
|
- [Install](#install)
|
|
45
32
|
- [Quick start](#quick-start)
|
|
46
|
-
- [
|
|
47
|
-
- [Landing carousels](#landing-carousels)
|
|
33
|
+
- [Package detail page](#package-detail-page)
|
|
34
|
+
- [Landing — all four carousels](#landing--all-four-carousels)
|
|
35
|
+
- [Landing — interleaved sections (FAQ between carousels)](#landing--interleaved-sections-faq-between-carousels)
|
|
36
|
+
- [Agency profile page](#agency-profile-page)
|
|
48
37
|
- [Just the data](#just-the-data)
|
|
49
|
-
- [
|
|
50
|
-
- [
|
|
38
|
+
- [Booking flow](#booking-flow)
|
|
39
|
+
- [Components reference](#components-reference)
|
|
40
|
+
- [Hooks reference](#hooks-reference)
|
|
51
41
|
- [Client configuration](#client-configuration)
|
|
52
|
-
- [SSR with Next.js](#ssr-with-nextjs)
|
|
53
|
-
- [Composable landing layouts](#composable-landing-layouts)
|
|
42
|
+
- [SSR with Next.js / Remix](#ssr-with-nextjs--remix)
|
|
54
43
|
- [Styling](#styling)
|
|
55
|
-
- [TypeScript](#typescript)
|
|
56
|
-
- [API
|
|
44
|
+
- [TypeScript types](#typescript-types)
|
|
45
|
+
- [API endpoints called](#api-endpoints-called)
|
|
46
|
+
- [Edge cases & FAQ](#edge-cases--faq)
|
|
47
|
+
- [Browser support](#browser-support)
|
|
48
|
+
- [Versioning & releases](#versioning--releases)
|
|
57
49
|
- [Support](#support)
|
|
58
50
|
- [License](#license)
|
|
59
51
|
|
|
@@ -65,19 +57,27 @@ That's a complete, production-grade package detail page: gallery, itinerary with
|
|
|
65
57
|
npm install @travories/frontend-sdk
|
|
66
58
|
```
|
|
67
59
|
|
|
68
|
-
**Peer dependencies** (skip if you only use the API client):
|
|
60
|
+
**Peer dependencies** (skip if you only use the API client / Node):
|
|
69
61
|
|
|
70
62
|
```bash
|
|
71
63
|
npm install react react-dom
|
|
72
64
|
```
|
|
73
65
|
|
|
74
|
-
|
|
66
|
+
| Requirement | Version |
|
|
67
|
+
|---|---|
|
|
68
|
+
| Node | ≥ 18 (uses built-in `fetch`) |
|
|
69
|
+
| React | ≥ 17 |
|
|
70
|
+
| TypeScript | optional but recommended ≥ 5 |
|
|
71
|
+
|
|
72
|
+
`leaflet` and `react-leaflet` are runtime deps, included automatically when you install the SDK — needed for the itinerary map.
|
|
75
73
|
|
|
76
74
|
---
|
|
77
75
|
|
|
78
76
|
## Quick start
|
|
79
77
|
|
|
80
|
-
###
|
|
78
|
+
### Package detail page
|
|
79
|
+
|
|
80
|
+
The all-in-one drop-in. Pass `client` + `slug` — the SDK fetches everything and renders.
|
|
81
81
|
|
|
82
82
|
```tsx
|
|
83
83
|
import { TravoriesClient, PackageDetailView } from "@travories/frontend-sdk";
|
|
@@ -91,16 +91,23 @@ function PackagePage({ slug }: { slug: string }) {
|
|
|
91
91
|
client={client}
|
|
92
92
|
slug={slug}
|
|
93
93
|
currency="USD"
|
|
94
|
-
onReserve={(pkg) => {
|
|
95
|
-
//
|
|
96
|
-
|
|
94
|
+
onReserve={async ({ pkg, arrival, travelers }) => {
|
|
95
|
+
// see "Booking flow" section below for the canonical pattern
|
|
96
|
+
const { data: bookingId } = await client.bookings.initiate({
|
|
97
|
+
packageId: pkg.slug,
|
|
98
|
+
arrivalDate: arrival,
|
|
99
|
+
travelers,
|
|
100
|
+
});
|
|
101
|
+
window.location.href = `/checkout/${bookingId}`;
|
|
97
102
|
}}
|
|
103
|
+
agencyHrefFor={(host) => `/agency/${host.slug}`}
|
|
104
|
+
openAgencyInNewTab
|
|
98
105
|
/>
|
|
99
106
|
);
|
|
100
107
|
}
|
|
101
108
|
```
|
|
102
109
|
|
|
103
|
-
### Landing carousels
|
|
110
|
+
### Landing — all four carousels
|
|
104
111
|
|
|
105
112
|
```tsx
|
|
106
113
|
import { TravoriesClient, HomeSections } from "@travories/frontend-sdk";
|
|
@@ -112,7 +119,6 @@ function Landing() {
|
|
|
112
119
|
return (
|
|
113
120
|
<HomeSections
|
|
114
121
|
client={client}
|
|
115
|
-
currency="USD"
|
|
116
122
|
hrefFor={(pkg) => `/package/${pkg.slug}`}
|
|
117
123
|
openInNewTab
|
|
118
124
|
/>
|
|
@@ -122,6 +128,55 @@ function Landing() {
|
|
|
122
128
|
|
|
123
129
|
Renders the four canonical sections: **Popular**, **Trending Right Now**, **Top Rated**, **More to Explore**.
|
|
124
130
|
|
|
131
|
+
### Landing — interleaved sections (FAQ between carousels)
|
|
132
|
+
|
|
133
|
+
When you want SDK sections mixed with your own content (hero, FAQ, banners), wrap with `<HomeSectionsProvider>` and drop `<PackagesSection>` anywhere inside. **All sections share one fetch.**
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import {
|
|
137
|
+
TravoriesClient,
|
|
138
|
+
HomeSectionsProvider,
|
|
139
|
+
PackagesSection,
|
|
140
|
+
} from "@travories/frontend-sdk";
|
|
141
|
+
|
|
142
|
+
const client = new TravoriesClient({ baseUrl: "https://api.travories.com" });
|
|
143
|
+
|
|
144
|
+
function Landing() {
|
|
145
|
+
return (
|
|
146
|
+
<HomeSectionsProvider client={client}>
|
|
147
|
+
<Hero /> {/* your own */}
|
|
148
|
+
<PackagesSection section="popular" hrefFor={...} openInNewTab />
|
|
149
|
+
<PackagesSection section="trending" hrefFor={...} openInNewTab />
|
|
150
|
+
<FAQ /> {/* your own */}
|
|
151
|
+
<PackagesSection section="topRated" title="Editor's picks" />
|
|
152
|
+
<PackagesSection section="moreToExplore" />
|
|
153
|
+
<Newsletter /> {/* your own */}
|
|
154
|
+
</HomeSectionsProvider>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Available section keys: `"popular" | "trending" | "topRated" | "moreToExplore"`.
|
|
160
|
+
|
|
161
|
+
### Agency profile page
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { TravoriesClient, AgencyDetailView } from "@travories/frontend-sdk";
|
|
165
|
+
|
|
166
|
+
function AgencyPage({ slug }: { slug: string }) {
|
|
167
|
+
return (
|
|
168
|
+
<AgencyDetailView
|
|
169
|
+
client={client}
|
|
170
|
+
slug={slug}
|
|
171
|
+
packageHrefFor={(pkg) => `/package/${pkg.slug}`}
|
|
172
|
+
openPackagesInNewTab
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Renders: breadcrumb → title → host card + overview + verified badge → partner associations → "Popular packages from {agency}" carousel.
|
|
179
|
+
|
|
125
180
|
### Just the data
|
|
126
181
|
|
|
127
182
|
```ts
|
|
@@ -129,74 +184,145 @@ import { TravoriesClient } from "@travories/frontend-sdk";
|
|
|
129
184
|
|
|
130
185
|
const client = new TravoriesClient({ baseUrl: "https://api.travories.com" });
|
|
131
186
|
|
|
132
|
-
const pkg
|
|
133
|
-
const bundle
|
|
134
|
-
//
|
|
135
|
-
const sections
|
|
187
|
+
const pkg = await client.getPackageBySlug("everest-base-camp");
|
|
188
|
+
const bundle = await client.getPackageBundle("everest-base-camp");
|
|
189
|
+
// ↑ pkg + host + attractions + per-day GeoJSON routes, in one call
|
|
190
|
+
const sections = await client.packages.getHomeSections();
|
|
191
|
+
const agency = await client.getAgencyBundle("travel-route-pvt-ltd");
|
|
192
|
+
const routes = await client.packages.getRoutes("everest-base-camp");
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Returns `null` on 404 / network errors by default (silent mode). Pass `{ silent: false }` to throw instead.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Booking flow
|
|
200
|
+
|
|
201
|
+
End-to-end booking takes 3 lines of consumer code:
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
<PackageDetailView
|
|
205
|
+
client={client}
|
|
206
|
+
slug={slug}
|
|
207
|
+
onReserve={async ({ pkg, arrival, travelers }) => {
|
|
208
|
+
const { data: bookingId } = await client.bookings.initiate({
|
|
209
|
+
packageId: pkg.slug,
|
|
210
|
+
arrivalDate: arrival, // ISO date string e.g. "2027-08-15"
|
|
211
|
+
travelers, // [{ name: "Adult", count: 2 }, …]
|
|
212
|
+
});
|
|
213
|
+
// bookingId is a UUID returned by the BE.
|
|
214
|
+
// Hand off to your payment flow:
|
|
215
|
+
window.location.href = `/checkout/${bookingId}`;
|
|
216
|
+
// …or open in a new tab:
|
|
217
|
+
// window.open(`/checkout/${bookingId}`, "_blank", "noopener,noreferrer");
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
136
220
|
```
|
|
137
221
|
|
|
138
|
-
|
|
222
|
+
**What happens visually:** the user picks a date + travelers in the built-in BookingCardLite. When they click **Book Now**, the button switches to **"Initiating…"** and disables itself until `onReserve` resolves (it auto-detects a returned `Promise`). On success, your callback navigates wherever it needs to. On error, throw or `alert` — the button re-enables automatically.
|
|
223
|
+
|
|
224
|
+
**Errors:** `client.bookings.initiate()` throws `TravoriesApiError` on a non-2xx response. Wrap in try/catch:
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
onReserve={async (payload) => {
|
|
228
|
+
try {
|
|
229
|
+
const { data } = await client.bookings.initiate({ ... });
|
|
230
|
+
navigateToCheckout(data);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
alert(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
233
|
+
}
|
|
234
|
+
}}
|
|
235
|
+
```
|
|
139
236
|
|
|
140
237
|
---
|
|
141
238
|
|
|
142
|
-
## Components
|
|
239
|
+
## Components reference
|
|
143
240
|
|
|
144
241
|
### Page-level drop-ins
|
|
145
242
|
|
|
146
|
-
| Component |
|
|
243
|
+
| Component | Renders |
|
|
147
244
|
|---|---|
|
|
148
|
-
| `<PackageDetailView />` | Full detail page
|
|
149
|
-
| `<
|
|
150
|
-
| `<
|
|
151
|
-
| `<
|
|
245
|
+
| `<PackageDetailView />` | Full package detail page |
|
|
246
|
+
| `<AgencyDetailView />` | Full agency profile page |
|
|
247
|
+
| `<HomeSections />` | All four landing carousels in one block |
|
|
248
|
+
| `<HomeSectionsProvider />` | Wraps your tree; fetches once; sub-sections share data |
|
|
249
|
+
| `<PackagesSection section="..." />` | One specific section, consumes the Provider |
|
|
152
250
|
|
|
153
|
-
###
|
|
251
|
+
### Sub-components for custom layouts
|
|
154
252
|
|
|
155
|
-
|
|
253
|
+
All exported individually:
|
|
156
254
|
|
|
157
255
|
| Component | Purpose |
|
|
158
256
|
|---|---|
|
|
159
|
-
| `<PackagesCarousel />` | Horizontal scrollable
|
|
160
|
-
| `<PackageCard />` | Single card
|
|
161
|
-
| `<TopSection />` | Detail-page header
|
|
162
|
-
| `<GallerySection />` | 5-image
|
|
163
|
-
| `<PackageOverview />` | Description + key facts panel
|
|
164
|
-
| `<Itinerary />` | Day tabs + day card + map (
|
|
165
|
-
| `<PackageMap />` | Leaflet
|
|
166
|
-
| `<PackageIncluded />` | What's-included checklist
|
|
167
|
-
| `<PackageExcludedAndToPack />` |
|
|
168
|
-
| `<HostCard
|
|
169
|
-
| `<
|
|
257
|
+
| `<PackagesCarousel title packages />` | Horizontal scrollable carousel with arrows |
|
|
258
|
+
| `<PackageCard pkg />` | Single card (image + title + duration · stars · price) |
|
|
259
|
+
| `<TopSection topData />` | Detail-page header (breadcrumb, title, rating, gallery) |
|
|
260
|
+
| `<GallerySection media />` | 5-image grid + click-to-open lightbox |
|
|
261
|
+
| `<PackageOverview description topData />` | Description + key facts panel |
|
|
262
|
+
| `<Itinerary Description routes />` | Day tabs + day card + map (sticky on scroll) |
|
|
263
|
+
| `<PackageMap locations routes dayNumber />` | Leaflet map with markers + GeoJSON polylines |
|
|
264
|
+
| `<PackageIncluded thingsToDo />` | What's-included checklist |
|
|
265
|
+
| `<PackageExcludedAndToPack thingsExcluded thingsToPack />` | Two checklists |
|
|
266
|
+
| `<HostCard hostData onAgencyClick />` | Agency card with stats |
|
|
267
|
+
| `<HostAbout hostName description />` | "About {host}" prose block |
|
|
268
|
+
| `<BookingCardLite DescriptionData onReserve />` | Booking sidebar |
|
|
269
|
+
| `<AgencyAssociations associations />` | Partner-badge strip |
|
|
270
|
+
|
|
271
|
+
### Shared `<PackageDetailView />` props
|
|
272
|
+
|
|
273
|
+
| Prop | Type | Notes |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| `client` | `TravoriesClient` | Required when not using `initialBundle` |
|
|
276
|
+
| `slug` | `string` | Required when not using `initialBundle` |
|
|
277
|
+
| `initialBundle` | `PackageBundle \| null` | Pre-fetched (SSR pattern). Skips the internal fetch |
|
|
278
|
+
| `currency` | `string` | Default `"USD"`. Anything `Intl.NumberFormat` accepts |
|
|
279
|
+
| `onReserve` | `(payload: ReservePayload) => void \| Promise<void>` | Return a Promise to enable the "Initiating…" CTA state |
|
|
280
|
+
| `onSelectHost` | `(host: PackageHost) => void` | Fired when host card is clicked |
|
|
281
|
+
| `agencyHrefFor` | `(host) => string` | If set, host card becomes an `<a href>` |
|
|
282
|
+
| `openAgencyInNewTab` | `boolean` | Adds `target="_blank"` on the host anchor |
|
|
283
|
+
| `className` | `string` | Extra class on root |
|
|
284
|
+
| `renderLoading` | `() => ReactNode` | Custom skeleton |
|
|
285
|
+
| `renderNotFound` | `() => ReactNode` | Custom 404 |
|
|
286
|
+
|
|
287
|
+
### Shared `<AgencyDetailView />` props
|
|
288
|
+
|
|
289
|
+
| Prop | Type | Notes |
|
|
290
|
+
|---|---|---|
|
|
291
|
+
| `client` / `slug` / `initialBundle` | — | Same dual mode as `<PackageDetailView />` |
|
|
292
|
+
| `currency` | `string` | For package card prices |
|
|
293
|
+
| `onSelectPackage` | `(pkg: HomePackageCard) => void` | Click handler |
|
|
294
|
+
| `packageHrefFor` | `(pkg) => string` | Renders package cards as anchors |
|
|
295
|
+
| `openPackagesInNewTab` | `boolean` | Adds `target="_blank"` |
|
|
296
|
+
| `renderLoading` / `renderNotFound` | — | Override defaults |
|
|
170
297
|
|
|
171
298
|
---
|
|
172
299
|
|
|
173
|
-
## Hooks
|
|
300
|
+
## Hooks reference
|
|
174
301
|
|
|
175
|
-
|
|
302
|
+
| Hook | Returns |
|
|
303
|
+
|---|---|
|
|
304
|
+
| `usePackageBySlug(client, slug)` | `{ data: TravoriesPackage \| null, loading, error, refetch }` |
|
|
305
|
+
| `usePackageBundle(client, slug)` | `{ data: PackageBundle \| null, loading, error, refetch }` |
|
|
306
|
+
| `useHomeSections(client)` | `{ data: HomeSectionsResponse \| null, loading, error, refetch }` |
|
|
307
|
+
| `useHomeSectionsContext()` | Same shape — reads from `<HomeSectionsProvider>` (shared fetch) |
|
|
308
|
+
| `useAgencyBundle(client, slug)` | `{ data: AgencyBundle \| null, loading, error, refetch }` |
|
|
176
309
|
|
|
177
|
-
|
|
178
|
-
import { usePackageBundle, useHomeSections } from "@travories/frontend-sdk";
|
|
310
|
+
All hooks accept `null` for `client` / `slug` to **stay inert** (no fetch). Use when you already have data via `initialBundle` and just need the hook to satisfy rules-of-hooks.
|
|
179
311
|
|
|
180
|
-
|
|
312
|
+
Example with custom UI:
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
function Custom({ slug }: { slug: string }) {
|
|
181
316
|
const { data, loading, error, refetch } = usePackageBundle(client, slug);
|
|
182
|
-
// data: { pkg, host, attractions, routes }
|
|
183
317
|
|
|
184
|
-
|
|
185
|
-
|
|
318
|
+
if (loading) return <MySpinner />;
|
|
319
|
+
if (error) return <button onClick={refetch}>Retry: {error.message}</button>;
|
|
320
|
+
if (!data) return <My404 />;
|
|
186
321
|
|
|
187
|
-
|
|
322
|
+
return <article><h1>{data.pkg.title}</h1>{/* ... */}</article>;
|
|
188
323
|
}
|
|
189
324
|
```
|
|
190
325
|
|
|
191
|
-
| Hook | Returns |
|
|
192
|
-
|---|---|
|
|
193
|
-
| `usePackageBySlug(client, slug)` | `{ data: TravoriesPackage | null, loading, error, refetch }` |
|
|
194
|
-
| `usePackageBundle(client, slug)` | `{ data: PackageBundle | null, loading, error, refetch }` |
|
|
195
|
-
| `useHomeSections(client)` | `{ data: HomeSectionsResponse | null, loading, error, refetch }` |
|
|
196
|
-
| `useHomeSectionsContext()` | Same as above, but reads from `<HomeSectionsProvider>` context (one shared fetch). |
|
|
197
|
-
|
|
198
|
-
All hooks accept `null` instead of a client to stay inert (useful when you already have data via `initialBundle` / `initialSections`).
|
|
199
|
-
|
|
200
326
|
---
|
|
201
327
|
|
|
202
328
|
## Client configuration
|
|
@@ -204,37 +330,40 @@ All hooks accept `null` instead of a client to stay inert (useful when you alrea
|
|
|
204
330
|
```ts
|
|
205
331
|
new TravoriesClient({
|
|
206
332
|
baseUrl: "https://api.travories.com", // required
|
|
207
|
-
apiKey: "tvr_xxx", // optional — sent as x-api-key
|
|
208
|
-
authToken: () => session?.accessToken, // optional — sent as Bearer
|
|
209
|
-
defaultHeaders: { "x-trace-id": "abc" }, // optional
|
|
333
|
+
apiKey: "tvr_xxx", // optional — sent as `x-api-key`
|
|
334
|
+
authToken: () => session?.accessToken, // optional — sent as `Bearer`
|
|
335
|
+
defaultHeaders: { "x-trace-id": "abc" }, // optional — merged into every request
|
|
210
336
|
fetchImpl: customFetch, // optional — supply your own fetch
|
|
211
337
|
});
|
|
212
338
|
```
|
|
213
339
|
|
|
214
|
-
`authToken` accepts a string
|
|
340
|
+
`authToken` accepts a **string**, a **sync function**, or an **async function** — useful when the token rotates per request.
|
|
215
341
|
|
|
216
|
-
|
|
342
|
+
### Available resources
|
|
217
343
|
|
|
218
344
|
```ts
|
|
219
|
-
|
|
345
|
+
client.packages // PackagesResource
|
|
346
|
+
client.agencies // AgenciesResource
|
|
347
|
+
client.bookings // BookingsResource
|
|
348
|
+
```
|
|
220
349
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
350
|
+
### Convenience shortcuts on the client itself
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
client.getPackageBySlug(slug)
|
|
354
|
+
client.getPackageBundle(slug)
|
|
355
|
+
client.getAgencyBySlug(slug)
|
|
356
|
+
client.getAgencyBundle(slug)
|
|
228
357
|
```
|
|
229
358
|
|
|
230
359
|
---
|
|
231
360
|
|
|
232
|
-
## SSR with Next.js
|
|
361
|
+
## SSR with Next.js / Remix
|
|
233
362
|
|
|
234
|
-
|
|
363
|
+
Fetch on the server, hand off via `initialBundle` / `initialSections`. The component skips its internal fetch and renders synchronously.
|
|
235
364
|
|
|
236
365
|
```tsx
|
|
237
|
-
// app/packages/[slug]/page.tsx
|
|
366
|
+
// app/packages/[slug]/page.tsx — Next.js App Router
|
|
238
367
|
import { TravoriesClient, PackageDetailView } from "@travories/frontend-sdk";
|
|
239
368
|
import "@travories/frontend-sdk/styles.css";
|
|
240
369
|
|
|
@@ -243,102 +372,59 @@ const client = new TravoriesClient({ baseUrl: process.env.TRAVORIES_API_URL! });
|
|
|
243
372
|
export default async function Page({ params }: { params: { slug: string } }) {
|
|
244
373
|
const bundle = await client.getPackageBundle(params.slug);
|
|
245
374
|
if (!bundle) return <NotFound />;
|
|
246
|
-
|
|
247
375
|
return <PackageDetailView initialBundle={bundle} />;
|
|
248
376
|
}
|
|
249
377
|
```
|
|
250
378
|
|
|
251
379
|
```tsx
|
|
252
|
-
// app/page.tsx
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const client = new TravoriesClient({ baseUrl: process.env.TRAVORIES_API_URL! });
|
|
257
|
-
|
|
258
|
-
export default async function Landing() {
|
|
259
|
-
const sections = await client.packages.getHomeSections();
|
|
260
|
-
return (
|
|
261
|
-
<HomeSections
|
|
262
|
-
initialSections={sections}
|
|
263
|
-
hrefFor={(p) => `/package/${p.slug}`}
|
|
264
|
-
/>
|
|
265
|
-
);
|
|
266
|
-
}
|
|
380
|
+
// app/page.tsx — landing
|
|
381
|
+
const sections = await client.packages.getHomeSections();
|
|
382
|
+
return <HomeSections initialSections={sections} hrefFor={...} />;
|
|
267
383
|
```
|
|
268
384
|
|
|
269
|
-
Works with Pages Router (`getServerSideProps`), Remix loaders, and React Router data API too — the pattern is the same.
|
|
270
|
-
|
|
271
|
-
---
|
|
272
|
-
|
|
273
|
-
## Composable landing layouts
|
|
274
|
-
|
|
275
|
-
When you want **SDK sections interleaved with your own content** (FAQs, testimonials, ads), wrap the page in `<HomeSectionsProvider>` and drop `<PackagesSection>` anywhere inside. Sections share one fetch.
|
|
276
|
-
|
|
277
385
|
```tsx
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
} from "@travories/frontend-sdk";
|
|
283
|
-
|
|
284
|
-
const client = new TravoriesClient({ baseUrl: "https://api.travories.com" });
|
|
285
|
-
|
|
286
|
-
function Landing() {
|
|
287
|
-
return (
|
|
288
|
-
<HomeSectionsProvider client={client}>
|
|
289
|
-
<Hero />
|
|
290
|
-
<PackagesSection section="popular" hrefFor={(p) => `/package/${p.slug}`} />
|
|
291
|
-
<PackagesSection section="trending" />
|
|
292
|
-
<FAQ /> {/* your own */}
|
|
293
|
-
<Newsletter /> {/* your own */}
|
|
294
|
-
<PackagesSection section="topRated" title="Editor's picks" />
|
|
295
|
-
<PackagesSection section="moreToExplore" />
|
|
296
|
-
<Footer />
|
|
297
|
-
</HomeSectionsProvider>
|
|
298
|
-
);
|
|
299
|
-
}
|
|
386
|
+
// app/agency/[slug]/page.tsx
|
|
387
|
+
const agency = await client.getAgencyBundle(params.slug);
|
|
388
|
+
if (!agency) return <NotFound />;
|
|
389
|
+
return <AgencyDetailView initialBundle={agency} packageHrefFor={...} />;
|
|
300
390
|
```
|
|
301
391
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
For a totally custom block, drop down to `useHomeSectionsContext()`:
|
|
305
|
-
|
|
306
|
-
```tsx
|
|
307
|
-
function StatsBar() {
|
|
308
|
-
const { data } = useHomeSectionsContext();
|
|
309
|
-
if (!data) return null;
|
|
310
|
-
return <div>{data.popular.length + data.trending.length} packages featured</div>;
|
|
311
|
-
}
|
|
312
|
-
```
|
|
392
|
+
Same shape works with Remix / React Router data loaders.
|
|
313
393
|
|
|
314
394
|
---
|
|
315
395
|
|
|
316
396
|
## Styling
|
|
317
397
|
|
|
318
|
-
The SDK ships precompiled
|
|
398
|
+
The SDK ships **precompiled CSS** scoped under a wrapper class — your app's styling is never affected.
|
|
319
399
|
|
|
320
400
|
```ts
|
|
321
401
|
import "@travories/frontend-sdk/styles.css";
|
|
322
402
|
```
|
|
323
403
|
|
|
324
|
-
**Scope guarantee**:
|
|
404
|
+
**Scope guarantee**: every utility class is generated under `.tvr-package-detail` selectors, so there's no collision risk with your own Tailwind / CSS Modules / whatever.
|
|
325
405
|
|
|
326
|
-
**Re-skinning
|
|
406
|
+
**Re-skinning**: override the design tokens with CSS:
|
|
327
407
|
|
|
328
408
|
```css
|
|
329
409
|
.tvr-package-detail {
|
|
330
|
-
/* CSS variables you can override */
|
|
331
410
|
--tvr-radius: 0.75rem;
|
|
411
|
+
/* …any other CSS vars you've added */
|
|
332
412
|
}
|
|
333
413
|
```
|
|
334
414
|
|
|
335
|
-
|
|
415
|
+
For more aggressive customization, increase specificity:
|
|
416
|
+
|
|
417
|
+
```css
|
|
418
|
+
.my-app .tvr-package-detail .text-primary-normal {
|
|
419
|
+
color: #1d4ed8;
|
|
420
|
+
}
|
|
421
|
+
```
|
|
336
422
|
|
|
337
423
|
---
|
|
338
424
|
|
|
339
|
-
## TypeScript
|
|
425
|
+
## TypeScript types
|
|
340
426
|
|
|
341
|
-
Every export ships with full
|
|
427
|
+
Every export ships with full `.d.ts` definitions. Notable types:
|
|
342
428
|
|
|
343
429
|
```ts
|
|
344
430
|
import type {
|
|
@@ -353,11 +439,21 @@ import type {
|
|
|
353
439
|
PackageRoutesDay,
|
|
354
440
|
HomePackageCard,
|
|
355
441
|
HomeSectionsResponse,
|
|
442
|
+
AgencyHostDetails,
|
|
443
|
+
AgencyAssociationItem,
|
|
444
|
+
AgencyBundle,
|
|
356
445
|
MajorAttractionItem,
|
|
357
446
|
TravoriesImage,
|
|
358
447
|
|
|
448
|
+
// Booking
|
|
449
|
+
BookingTraveler,
|
|
450
|
+
BookingInitiateRequest,
|
|
451
|
+
BookingInitiateResponse,
|
|
452
|
+
ReservePayload,
|
|
453
|
+
|
|
359
454
|
// Component props
|
|
360
455
|
PackageDetailViewProps,
|
|
456
|
+
AgencyDetailViewProps,
|
|
361
457
|
HomeSectionsProps,
|
|
362
458
|
HomeSectionsProviderProps,
|
|
363
459
|
PackagesSectionProps,
|
|
@@ -367,8 +463,9 @@ import type {
|
|
|
367
463
|
UsePackageBySlugResult,
|
|
368
464
|
UsePackageBundleResult,
|
|
369
465
|
UseHomeSectionsResult,
|
|
466
|
+
UseAgencyBundleResult,
|
|
370
467
|
|
|
371
|
-
//
|
|
468
|
+
// HTTP
|
|
372
469
|
HttpClientConfig,
|
|
373
470
|
HttpRequestOptions,
|
|
374
471
|
} from "@travories/frontend-sdk";
|
|
@@ -376,28 +473,96 @@ import type {
|
|
|
376
473
|
|
|
377
474
|
---
|
|
378
475
|
|
|
379
|
-
## API
|
|
476
|
+
## API endpoints called
|
|
380
477
|
|
|
381
|
-
|
|
478
|
+
For network/CORS troubleshooting, here's exactly what each method hits:
|
|
382
479
|
|
|
383
|
-
| Method | Endpoint |
|
|
384
|
-
|
|
385
|
-
| `getBySlug(slug)` | `GET /agency-package/by-slug/:slug` |
|
|
386
|
-
| `getHost(slug)` | `GET /agency-info/by-package-slug/:slug` |
|
|
387
|
-
| `getMajorAttractions(slug)` | `GET /major-attractions/package/:slug` |
|
|
388
|
-
| `getRoutes(slug)` | `GET /agency-package/routes/:slug` |
|
|
389
|
-
| `getHomeSections()` | `GET /agency-package/home/sections` |
|
|
390
|
-
| `getBundle(slug)` | parallel
|
|
480
|
+
| Method | Endpoint |
|
|
481
|
+
|---|---|
|
|
482
|
+
| `client.packages.getBySlug(slug)` | `GET /agency-package/by-slug/:slug` |
|
|
483
|
+
| `client.packages.getHost(slug)` | `GET /agency-info/by-package-slug/:slug` |
|
|
484
|
+
| `client.packages.getMajorAttractions(slug)` | `GET /major-attractions/package/:slug` |
|
|
485
|
+
| `client.packages.getRoutes(slug)` | `GET /agency-package/routes/:slug` |
|
|
486
|
+
| `client.packages.getHomeSections()` | `GET /agency-package/home/sections` |
|
|
487
|
+
| `client.packages.getBundle(slug)` | parallel: by-slug + host + attractions + routes |
|
|
488
|
+
| `client.agencies.getBySlug(slug)` | `GET /agency-info/by-slug/:slug` |
|
|
489
|
+
| `client.agencies.getAssociations(slug)` | `GET /agency-association/agency/slug/:slug` |
|
|
490
|
+
| `client.agencies.getPackages(slug)` | `GET /agency-package/by-agency/:slug?limit&page` |
|
|
491
|
+
| `client.agencies.getBundle(slug)` | parallel: 3 calls above |
|
|
492
|
+
| `client.bookings.initiate(payload)` | `POST /booking/initiate` |
|
|
493
|
+
|
|
494
|
+
All `GET`s are silent by default (return `null` on 404 / network error). The `POST` throws `TravoriesApiError` so you can surface failures to the user.
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Edge cases & FAQ
|
|
499
|
+
|
|
500
|
+
**Q: What happens if the package slug doesn't exist?**
|
|
501
|
+
A: `<PackageDetailView>` shows the not-found state (override with `renderNotFound`). Hooks return `{ data: null, loading: false, error: null }`.
|
|
502
|
+
|
|
503
|
+
**Q: What if the API is unreachable / CORS / network down?**
|
|
504
|
+
A: Silent endpoints log to the console and return `null`. Components display the not-found state. The booking endpoint (POST) throws, so wrap it in try/catch.
|
|
505
|
+
|
|
506
|
+
**Q: How do I show "Sold out" / disable Reserve?**
|
|
507
|
+
A: Don't pass `onReserve` — the button auto-disables and shows "Contact host to book".
|
|
508
|
+
|
|
509
|
+
**Q: My consumer doesn't have auth yet — how do I hide the wishlist heart?**
|
|
510
|
+
A: It's already hidden by default. Opt in by passing `showHeart` + `liked` + `onLike` to `<PackageCard>`.
|
|
511
|
+
|
|
512
|
+
**Q: Can I show only one section from the home payload?**
|
|
513
|
+
A: Yes — either filter via `<HomeSections order={["trending"]} />` or use `<PackagesSection section="trending" />` inside a `<HomeSectionsProvider>`.
|
|
514
|
+
|
|
515
|
+
**Q: Does the SDK fetch data multiple times if I render `<HomeSections>` and `<PackageDetailView>` on the same page?**
|
|
516
|
+
A: Each component does its own fetch independently. Use the Provider pattern for `HomeSections` to share its fetch across multiple `<PackagesSection>`. Package detail and agency detail each do their own bundle fetch.
|
|
517
|
+
|
|
518
|
+
**Q: How do I get the user to a new tab on card click?**
|
|
519
|
+
A: Pass `hrefFor={(p) => "/package/" + p.slug}` + `openInNewTab` on `<PackagesCarousel>` / `<PackagesSection>` / `<HomeSections>`. Cards render as real `<a href target="_blank">` so right-click → "Open in new tab" also works.
|
|
520
|
+
|
|
521
|
+
**Q: Can I change a section's title?**
|
|
522
|
+
A: Pass `title` to `<PackagesSection>`, or use the `titles` map on `<HomeSections>`.
|
|
523
|
+
|
|
524
|
+
**Q: How do I handle currency conversion?**
|
|
525
|
+
A: Pass `currency="EUR"` etc. — uses `Intl.NumberFormat` under the hood. The SDK doesn't convert; it just formats the amount the BE returned.
|
|
526
|
+
|
|
527
|
+
**Q: Can I use the SDK on the server only (no React)?**
|
|
528
|
+
A: Yes. Import only the client + types: `import { TravoriesClient } from "@travories/frontend-sdk"`. The React pieces are tree-shaken out if you never touch them.
|
|
529
|
+
|
|
530
|
+
**Q: Does it work in Edge runtimes (Vercel Edge, Cloudflare Workers)?**
|
|
531
|
+
A: Yes — the client uses only the built-in `fetch`. No Node-specific APIs.
|
|
532
|
+
|
|
533
|
+
**Q: Bundle size?**
|
|
534
|
+
A: ~100 KB gzipped (ESM, including Leaflet which is the heaviest dep). Tree-shakeable — Node-only consumers get ~10 KB.
|
|
535
|
+
|
|
536
|
+
**Q: How do I pre-warm the Leaflet map for faster perceived load?**
|
|
537
|
+
A: Map only mounts client-side (it's wrapped in an effect that imports `react-leaflet` on demand). Nothing to pre-warm.
|
|
538
|
+
|
|
539
|
+
**Q: My styles look broken — what's wrong?**
|
|
540
|
+
A: Make sure you imported `@travories/frontend-sdk/styles.css` exactly once at your app entry point. Don't import multiple times — that's fine but wasteful.
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Browser support
|
|
545
|
+
|
|
546
|
+
Last 2 versions of Chrome, Firefox, Safari, Edge. IE 11 is not supported.
|
|
547
|
+
|
|
548
|
+
`fetch`, `URL`, `IntersectionObserver`, CSS Grid, and `Intl.NumberFormat` are required.
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
## Versioning & releases
|
|
553
|
+
|
|
554
|
+
The SDK uses [semver](https://semver.org/). While < `1.0`, **breaking changes** are released as **minor** bumps (e.g. `0.1.x` → `0.2.0`), not majors. At `1.0` we switch to strict semver.
|
|
391
555
|
|
|
392
|
-
|
|
556
|
+
Release cadence is on-demand. Subscribe to the [releases page](https://github.com/Travories/frontend-sdk/releases) for notifications.
|
|
393
557
|
|
|
394
558
|
---
|
|
395
559
|
|
|
396
560
|
## Support
|
|
397
561
|
|
|
398
|
-
- **
|
|
399
|
-
- **
|
|
400
|
-
- **API
|
|
562
|
+
- **Source** / **issues**: [github.com/Travories/frontend-sdk](https://github.com/Travories/frontend-sdk)
|
|
563
|
+
- **Demo**: [github.com/Travories/frontend-sdk-preview](https://github.com/Travories/frontend-sdk-preview) — clone + `npm install` + `npm run dev`
|
|
564
|
+
- **API** (rate limits, auth, raw responses): [api.travories.com](https://api.travories.com)
|
|
565
|
+
- **Production site**: [travories.com](https://travories.com)
|
|
401
566
|
|
|
402
567
|
---
|
|
403
568
|
|
package/package.json
CHANGED