@qite/tide-components 1.0.2 → 1.0.3
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/build/build-cjs/index.js +80 -58
- package/build/build-cjs/src/search-results/components/traject/traject-node-card.d.ts +2 -0
- package/build/build-cjs/src/search-results/utils/traject-utils.d.ts +4 -2
- package/build/build-esm/index.js +80 -58
- package/build/build-esm/src/search-results/components/traject/traject-node-card.d.ts +2 -0
- package/build/build-esm/src/search-results/utils/traject-utils.d.ts +4 -2
- package/package.json +2 -2
package/build/build-cjs/index.js
CHANGED
|
@@ -32967,6 +32967,8 @@ const buildDestination = (line) => {
|
|
|
32967
32967
|
return { id: line.region.id, isRegion: true };
|
|
32968
32968
|
if (line.country?.id)
|
|
32969
32969
|
return { id: line.country.id, isCountry: true };
|
|
32970
|
+
if (line.latitude && line.longitude)
|
|
32971
|
+
return { latitude: line.latitude, longitude: line.longitude };
|
|
32970
32972
|
return { id: 0 };
|
|
32971
32973
|
};
|
|
32972
32974
|
const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
|
|
@@ -32986,10 +32988,27 @@ const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
|
|
|
32986
32988
|
rooms: context.rooms,
|
|
32987
32989
|
tagIds: []
|
|
32988
32990
|
});
|
|
32989
|
-
const
|
|
32990
|
-
const
|
|
32991
|
-
|
|
32991
|
+
const getNodeCandidates = (node) => {
|
|
32992
|
+
const configured = (node.alternatives ?? []).filter((x) => !!x.code && x.code.trim().length > 0);
|
|
32993
|
+
if (configured.length)
|
|
32994
|
+
return configured;
|
|
32995
|
+
const fallbackCode = node.code ?? node.preferredAccommodationCode;
|
|
32996
|
+
if (!fallbackCode || !fallbackCode.trim().length)
|
|
32997
|
+
return [];
|
|
32998
|
+
return [
|
|
32999
|
+
{
|
|
33000
|
+
code: fallbackCode,
|
|
33001
|
+
name: node.name,
|
|
33002
|
+
description: node.description,
|
|
33003
|
+
imageUrl: node.imageUrl,
|
|
33004
|
+
contentSource: node.contentSource,
|
|
33005
|
+
vendor: node.vendor,
|
|
33006
|
+
externalVendorId: node.externalVendorId,
|
|
33007
|
+
isPreferred: true
|
|
33008
|
+
}
|
|
33009
|
+
];
|
|
32992
33010
|
};
|
|
33011
|
+
const findCandidateForResult = (node, result) => getNodeCandidates(node).find((candidate) => matchesCode(result, candidate.code)) ?? null;
|
|
32993
33012
|
const matchesCode = (result, code) => {
|
|
32994
33013
|
if (result.code === code)
|
|
32995
33014
|
return true;
|
|
@@ -33089,10 +33108,26 @@ const getLocation = (result) => {
|
|
|
33089
33108
|
return result.countryName ?? '';
|
|
33090
33109
|
return result.countryName ? `${place}, ${result.countryName}` : place;
|
|
33091
33110
|
};
|
|
33092
|
-
const
|
|
33111
|
+
const toPlainText = (value) => {
|
|
33112
|
+
if (!value)
|
|
33113
|
+
return '';
|
|
33114
|
+
return he
|
|
33115
|
+
.decode(value.replace(/<[^>]*>/g, ' '))
|
|
33116
|
+
.replace(/\s+/g, ' ')
|
|
33117
|
+
.trim();
|
|
33118
|
+
};
|
|
33119
|
+
const TrajectNodeCard = ({ item, result, isSelected, languageCode, translations, onSelect, showNights }) => {
|
|
33120
|
+
const { node } = item;
|
|
33093
33121
|
const selectedOption = lodash.first(result.rooms)?.options?.find((x) => x.isSelected) ?? lodash.first(result.rooms)?.options?.[0];
|
|
33094
33122
|
const price = formatPrice$3(result.price, result.currencyCode, languageCode);
|
|
33095
33123
|
const nights = calculateNights(new Date(result.fromDate), new Date(result.toDate));
|
|
33124
|
+
const candidate = findCandidateForResult(node, result);
|
|
33125
|
+
const image = candidate?.imageUrl ?? null;
|
|
33126
|
+
const description = toPlainText(candidate?.description);
|
|
33127
|
+
const title = candidate?.name ?? result.name;
|
|
33128
|
+
const priceBlock = (React__default["default"].createElement("div", { className: "search__result-card__price__wrapper" },
|
|
33129
|
+
React__default["default"].createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
|
|
33130
|
+
React__default["default"].createElement("span", { className: "search__result-card__price" }, price)));
|
|
33096
33131
|
const selectButton = (React__default["default"].createElement("button", { type: "button", className: `cta ${isSelected ? 'cta--selected' : 'cta--select'}`, onClick: onSelect }, isSelected ? translations?.SHARED.SELECTED : translations?.SHARED.SELECT));
|
|
33097
33132
|
if (result.contents?.length) {
|
|
33098
33133
|
return (React__default["default"].createElement("div", { className: `search__result-card__wrapper search__result-card__wrapper--custom ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
@@ -33100,21 +33135,22 @@ const TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSel
|
|
|
33100
33135
|
React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton)));
|
|
33101
33136
|
}
|
|
33102
33137
|
return (React__default["default"].createElement("div", { className: `search__result-card__wrapper ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
33138
|
+
image && (React__default["default"].createElement("div", { className: "search__result-card__img-wrapper" },
|
|
33139
|
+
React__default["default"].createElement("img", { src: image, alt: title, className: "search__result-card__img" }),
|
|
33140
|
+
priceBlock)),
|
|
33103
33141
|
React__default["default"].createElement("div", { className: "search__result-card__content" },
|
|
33104
33142
|
React__default["default"].createElement("div", { className: "search__result-card__content__wrapper" },
|
|
33105
33143
|
React__default["default"].createElement("div", { className: "search__result-card__header" },
|
|
33106
33144
|
React__default["default"].createElement("div", { className: "search__result-card__header__wrapper" },
|
|
33107
33145
|
!!result.stars && (React__default["default"].createElement("div", { className: "rating" }, [...Array(result.stars)].map((_, index) => (React__default["default"].createElement(Icon, { name: "ui-star", key: `rating-star-${index + 1}`, width: 14, height: 14 }))))),
|
|
33108
|
-
React__default["default"].createElement("h3", { className: "search__result-card__title" },
|
|
33109
|
-
|
|
33110
|
-
React__default["default"].createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
|
|
33111
|
-
React__default["default"].createElement("span", { className: "search__result-card__price" }, price))),
|
|
33146
|
+
React__default["default"].createElement("h3", { className: "search__result-card__title" }, title)),
|
|
33147
|
+
!image && priceBlock),
|
|
33112
33148
|
React__default["default"].createElement("span", { className: "search__result-card__location" },
|
|
33113
33149
|
React__default["default"].createElement(Icon, { name: "ui-location", height: 16 }),
|
|
33114
33150
|
getLocation(result)),
|
|
33115
33151
|
React__default["default"].createElement("div", { className: "search__result-card__options" },
|
|
33116
33152
|
showNights && nights > 0 && (React__default["default"].createElement("div", { className: "search__result-card__option" },
|
|
33117
|
-
React__default["default"].createElement(Icon, { name: "ui-
|
|
33153
|
+
React__default["default"].createElement(Icon, { name: "ui-moon", height: 16 }),
|
|
33118
33154
|
nights,
|
|
33119
33155
|
" ",
|
|
33120
33156
|
translations?.SRP.NIGHTS)),
|
|
@@ -33123,7 +33159,8 @@ const TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSel
|
|
|
33123
33159
|
selectedOption.accommodationName)),
|
|
33124
33160
|
selectedOption?.regimeName && (React__default["default"].createElement("div", { className: "search__result-card__option" },
|
|
33125
33161
|
React__default["default"].createElement(Icon, { name: "ui-utensils", height: 16 }),
|
|
33126
|
-
selectedOption.regimeName)))
|
|
33162
|
+
selectedOption.regimeName))),
|
|
33163
|
+
description && React__default["default"].createElement("p", { className: "search__result-card__description" }, description)),
|
|
33127
33164
|
React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton))));
|
|
33128
33165
|
};
|
|
33129
33166
|
|
|
@@ -33179,7 +33216,8 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33179
33216
|
const isExpanded = !!expandedNodes[line.guid];
|
|
33180
33217
|
const results = availability?.results ?? [];
|
|
33181
33218
|
const selected = results.find((x) => x.code === availability?.selectedCode) ?? null;
|
|
33182
|
-
const
|
|
33219
|
+
const configured = results.filter((x) => x.code !== selected?.code && !!findCandidateForResult(node, x));
|
|
33220
|
+
const others = results.filter((x) => x.code !== selected?.code && !findCandidateForResult(node, x));
|
|
33183
33221
|
// No entry yet means the node's search has been queued but not dispatched — still loading.
|
|
33184
33222
|
const status = availability?.status ?? 'loading';
|
|
33185
33223
|
return (React__default["default"].createElement(React__default["default"].Fragment, { key: line.guid },
|
|
@@ -33188,9 +33226,7 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33188
33226
|
React__default["default"].createElement("p", { className: "search__results__label__date-date" }, dateFns.format(new Date(line.from), 'd', { locale })),
|
|
33189
33227
|
React__default["default"].createElement("p", null, dateFns.format(new Date(line.from), 'MMM', { locale }))),
|
|
33190
33228
|
React__default["default"].createElement("div", { className: "search__results__label__text" },
|
|
33191
|
-
React__default["default"].createElement(Icon, { name: getNodeIcon(node), height: 16 }),
|
|
33192
|
-
React__default["default"].createElement("h3", null,
|
|
33193
|
-
React__default["default"].createElement("strong", null, node.name)))),
|
|
33229
|
+
React__default["default"].createElement(Icon, { name: getNodeIcon(node), height: 16 }))),
|
|
33194
33230
|
status === 'loading' && (React__default["default"].createElement(Spinner, { label: node.serviceType === EXCURSION_SERVICE_TYPE ? translations.SRP.LOADING_EXCURSIONS : translations.SRP.LOADING_ACCOMMODATIONS })),
|
|
33195
33231
|
status === 'unavailable' && React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
|
|
33196
33232
|
status === 'resolved' && selected && (React__default["default"].createElement(React__default["default"].Fragment, null,
|
|
@@ -33199,12 +33235,9 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33199
33235
|
node.preferredAccommodationName,
|
|
33200
33236
|
" \u2014 ",
|
|
33201
33237
|
translations.SRP.NO_RESULTS))),
|
|
33202
|
-
React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--extended" },
|
|
33203
|
-
|
|
33204
|
-
isExpanded
|
|
33205
|
-
alternatives.map((result) => (React__default["default"].createElement(TrajectNodeCard, { key: result.code, result: result, isSelected: false, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, result.code), showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE })))),
|
|
33206
|
-
alternatives.length > 0 && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
|
|
33207
|
-
React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => toggleExpanded(line.guid) }, isExpanded ? translations.SRP.SHOW_LESS : `${translations.SRP.SHOW_MORE} (${alternatives.length})`)))))));
|
|
33238
|
+
React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--extended" }, [selected, ...configured, ...(isExpanded ? others : [])].map((result) => (React__default["default"].createElement(TrajectNodeCard, { key: result.code, item: item, result: result, isSelected: result.code === selected.code, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, result.code), showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE })))),
|
|
33239
|
+
others.length > 0 && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
|
|
33240
|
+
React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => toggleExpanded(line.guid) }, isExpanded ? translations.SRP.SHOW_LESS : `${translations.SRP.SHOW_MORE} (${others.length})`)))))));
|
|
33208
33241
|
};
|
|
33209
33242
|
const renderFlightNode = (item) => {
|
|
33210
33243
|
const { node, line } = item;
|
|
@@ -33245,36 +33278,23 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33245
33278
|
selectedFlight && (React__default["default"].createElement(IndependentFlightOption, { key: `flight-${selectedKey}`, item: isOutbound ? selectedFlight.outward : selectedFlight.return, guid: selectedFlight.outwardGuid, selectedGuid: selectedFlight.outwardGuid, isOutward: isOutbound, showSelectedState: true, price: selectedFlight.price, onSelect: isOutbound ? () => dispatch(setSelectedOutwardKey(null)) : undefined })),
|
|
33246
33279
|
visible.map((result) => (React__default["default"].createElement(IndependentFlightOption, { key: `flight-${result.outwardGuid}`, item: isOutbound ? result.outward : result.return, guid: result.outwardGuid, isOutward: isOutbound, price: result.price, currentSelectedPrice: selectedFlight?.price, onSelect: () => dispatch(isOutbound ? setSelectedOutwardKey(getFlightKey(result.outward.segments)) : setSelectedReturnKey(getFlightKey(result.return.segments))) })))))));
|
|
33247
33280
|
};
|
|
33248
|
-
/** Connecting markers and points of interest: shown for context, nothing to book. */
|
|
33249
|
-
const renderContextNode = (node) => (React__default["default"].createElement("div", { className: "search__results__cards__actions", key: `${node.nodeId}-context` },
|
|
33250
|
-
React__default["default"].createElement("span", null,
|
|
33251
|
-
React__default["default"].createElement(Icon, { name: "ui-car", height: 14 }),
|
|
33252
|
-
" ",
|
|
33253
|
-
node.name,
|
|
33254
|
-
node.distanceInKm ? ` — ${Math.round(node.distanceInKm)} km` : '')));
|
|
33255
33281
|
if (isLoading)
|
|
33256
33282
|
return React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
|
|
33257
33283
|
return (React__default["default"].createElement(React__default["default"].Fragment, null, trajectEntry.traject.days.map((day) => {
|
|
33258
33284
|
const dayNodes = nodesByDay.get(day.order) ?? [];
|
|
33259
33285
|
if (!dayNodes.length)
|
|
33260
33286
|
return null;
|
|
33261
|
-
return (React__default["default"].createElement(React__default["default"].Fragment, { key: `day-${day.order}` },
|
|
33262
|
-
|
|
33263
|
-
|
|
33264
|
-
|
|
33265
|
-
|
|
33266
|
-
|
|
33267
|
-
|
|
33268
|
-
|
|
33269
|
-
|
|
33270
|
-
|
|
33271
|
-
|
|
33272
|
-
return renderContextNode(node);
|
|
33273
|
-
const item = lineByNodeId.get(node.nodeId);
|
|
33274
|
-
if (!item)
|
|
33275
|
-
return renderContextNode(node);
|
|
33276
|
-
return isTrajectFlightNode(node) ? renderFlightNode(item) : renderServiceNode(item);
|
|
33277
|
-
})));
|
|
33287
|
+
return (React__default["default"].createElement(React__default["default"].Fragment, { key: `day-${day.order}` }, dayNodes.map((node) => {
|
|
33288
|
+
// if (node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) return null;
|
|
33289
|
+
// if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE) return renderContextNode(node);
|
|
33290
|
+
if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE)
|
|
33291
|
+
return null;
|
|
33292
|
+
const item = lineByNodeId.get(node.nodeId);
|
|
33293
|
+
// if (!item) return renderContextNode(node);
|
|
33294
|
+
if (!item)
|
|
33295
|
+
return null;
|
|
33296
|
+
return isTrajectFlightNode(node) ? renderFlightNode(item) : renderServiceNode(item);
|
|
33297
|
+
})));
|
|
33278
33298
|
})));
|
|
33279
33299
|
};
|
|
33280
33300
|
|
|
@@ -33308,25 +33328,27 @@ const useTrajectAvailability = () => {
|
|
|
33308
33328
|
const search = item.node.serviceType === EXCURSION_SERVICE_TYPE ? build.searchPackagingExcursions : build.searchPackagingAccommodations;
|
|
33309
33329
|
dispatch(setTrajectNodeLoading(lineGuid));
|
|
33310
33330
|
try {
|
|
33311
|
-
const
|
|
33312
|
-
|
|
33313
|
-
|
|
33314
|
-
|
|
33315
|
-
|
|
33316
|
-
const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, code), controller.signal);
|
|
33317
|
-
const hit = (attempt ?? []).find((result) => matchesCode(result, code));
|
|
33318
|
-
if (hit) {
|
|
33319
|
-
results = attempt;
|
|
33320
|
-
picked = hit;
|
|
33321
|
-
break;
|
|
33331
|
+
const candidates = getNodeCandidates(item.node);
|
|
33332
|
+
const attempts = await Promise.all(candidates.map(async (candidate) => {
|
|
33333
|
+
try {
|
|
33334
|
+
const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, candidate.code), controller.signal);
|
|
33335
|
+
return (attempt ?? []).find((result) => matchesCode(result, candidate.code)) ?? null;
|
|
33322
33336
|
}
|
|
33323
|
-
|
|
33324
|
-
|
|
33337
|
+
catch (error) {
|
|
33338
|
+
if (!controller.signal.aborted)
|
|
33339
|
+
console.error('Traject hotel unavailable', candidate.code, error);
|
|
33340
|
+
return null;
|
|
33341
|
+
}
|
|
33342
|
+
}));
|
|
33343
|
+
let results = attempts.filter((x) => !!x).filter((x, i, all) => all.findIndex((y) => y.code === x.code) === i);
|
|
33344
|
+
let picked = results[0] ?? null;
|
|
33345
|
+
const preferredCandidate = candidates.find((x) => x.isPreferred) ?? candidates[0];
|
|
33346
|
+
let preferredUnavailable = !!preferredCandidate && !results.some((x) => matchesCode(x, preferredCandidate.code));
|
|
33325
33347
|
// Nothing configured was available — fall back to the cheapest result in the destination.
|
|
33326
33348
|
if (!picked) {
|
|
33327
33349
|
results = (await search(config, buildTrajectNodeRequest(item, searchContext, ''), controller.signal)) ?? [];
|
|
33328
33350
|
picked = pickCheapest(results);
|
|
33329
|
-
preferredUnavailable =
|
|
33351
|
+
preferredUnavailable = candidates.length > 0;
|
|
33330
33352
|
}
|
|
33331
33353
|
if (controller.signal.aborted)
|
|
33332
33354
|
return null;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { PackagingAccommodationResponse } from '@qite/tide-client';
|
|
3
|
+
import { TrajectNodeWithLine } from '../../utils/traject-utils';
|
|
3
4
|
interface TrajectNodeCardProps {
|
|
5
|
+
item: TrajectNodeWithLine;
|
|
4
6
|
result: PackagingAccommodationResponse;
|
|
5
7
|
isSelected: boolean;
|
|
6
8
|
languageCode?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { PackagingAccommodationRequest, PackagingAccommodationResponse, PackagingEntry, PackagingEntryLine, PackagingFlightResponse, PackagingRoom, PackagingTrajectNode, PackagingTrajectResponse } from '@qite/tide-client';
|
|
1
|
+
import { PackagingAccommodationRequest, PackagingAccommodationResponse, PackagingEntry, PackagingEntryLine, PackagingFlightResponse, PackagingRoom, PackagingTrajectAlternative, PackagingTrajectNode, PackagingTrajectResponse } from '@qite/tide-client';
|
|
2
2
|
export declare const REGULAR_TRAJECT_NODE_TYPE = 0;
|
|
3
|
+
export declare const CONNECTING_TRAJECT_NODE_TYPE = 1;
|
|
3
4
|
export interface TrajectNodeWithLine {
|
|
4
5
|
node: PackagingTrajectNode;
|
|
5
6
|
line: PackagingEntryLine;
|
|
@@ -18,7 +19,8 @@ export interface TrajectSearchContext {
|
|
|
18
19
|
rooms: PackagingAccommodationRequest['rooms'];
|
|
19
20
|
}
|
|
20
21
|
export declare const buildTrajectNodeRequest: ({ node, line }: TrajectNodeWithLine, context: TrajectSearchContext, productCode: string) => PackagingAccommodationRequest;
|
|
21
|
-
export declare const
|
|
22
|
+
export declare const getNodeCandidates: (node: PackagingTrajectNode) => PackagingTrajectAlternative[];
|
|
23
|
+
export declare const findCandidateForResult: (node: PackagingTrajectNode, result: PackagingAccommodationResponse) => PackagingTrajectAlternative | null;
|
|
22
24
|
export declare const matchesCode: (result: PackagingAccommodationResponse, code: string) => boolean;
|
|
23
25
|
export declare const getPackagingRoomsFromEntry: (entry: PackagingEntry) => PackagingRoom[];
|
|
24
26
|
export declare const pickCheapest: (results: PackagingAccommodationResponse[]) => PackagingAccommodationResponse | null;
|
package/build/build-esm/index.js
CHANGED
|
@@ -32938,6 +32938,8 @@ const buildDestination = (line) => {
|
|
|
32938
32938
|
return { id: line.region.id, isRegion: true };
|
|
32939
32939
|
if (line.country?.id)
|
|
32940
32940
|
return { id: line.country.id, isCountry: true };
|
|
32941
|
+
if (line.latitude && line.longitude)
|
|
32942
|
+
return { latitude: line.latitude, longitude: line.longitude };
|
|
32941
32943
|
return { id: 0 };
|
|
32942
32944
|
};
|
|
32943
32945
|
const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
|
|
@@ -32957,10 +32959,27 @@ const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
|
|
|
32957
32959
|
rooms: context.rooms,
|
|
32958
32960
|
tagIds: []
|
|
32959
32961
|
});
|
|
32960
|
-
const
|
|
32961
|
-
const
|
|
32962
|
-
|
|
32962
|
+
const getNodeCandidates = (node) => {
|
|
32963
|
+
const configured = (node.alternatives ?? []).filter((x) => !!x.code && x.code.trim().length > 0);
|
|
32964
|
+
if (configured.length)
|
|
32965
|
+
return configured;
|
|
32966
|
+
const fallbackCode = node.code ?? node.preferredAccommodationCode;
|
|
32967
|
+
if (!fallbackCode || !fallbackCode.trim().length)
|
|
32968
|
+
return [];
|
|
32969
|
+
return [
|
|
32970
|
+
{
|
|
32971
|
+
code: fallbackCode,
|
|
32972
|
+
name: node.name,
|
|
32973
|
+
description: node.description,
|
|
32974
|
+
imageUrl: node.imageUrl,
|
|
32975
|
+
contentSource: node.contentSource,
|
|
32976
|
+
vendor: node.vendor,
|
|
32977
|
+
externalVendorId: node.externalVendorId,
|
|
32978
|
+
isPreferred: true
|
|
32979
|
+
}
|
|
32980
|
+
];
|
|
32963
32981
|
};
|
|
32982
|
+
const findCandidateForResult = (node, result) => getNodeCandidates(node).find((candidate) => matchesCode(result, candidate.code)) ?? null;
|
|
32964
32983
|
const matchesCode = (result, code) => {
|
|
32965
32984
|
if (result.code === code)
|
|
32966
32985
|
return true;
|
|
@@ -33060,10 +33079,26 @@ const getLocation = (result) => {
|
|
|
33060
33079
|
return result.countryName ?? '';
|
|
33061
33080
|
return result.countryName ? `${place}, ${result.countryName}` : place;
|
|
33062
33081
|
};
|
|
33063
|
-
const
|
|
33082
|
+
const toPlainText = (value) => {
|
|
33083
|
+
if (!value)
|
|
33084
|
+
return '';
|
|
33085
|
+
return he
|
|
33086
|
+
.decode(value.replace(/<[^>]*>/g, ' '))
|
|
33087
|
+
.replace(/\s+/g, ' ')
|
|
33088
|
+
.trim();
|
|
33089
|
+
};
|
|
33090
|
+
const TrajectNodeCard = ({ item, result, isSelected, languageCode, translations, onSelect, showNights }) => {
|
|
33091
|
+
const { node } = item;
|
|
33064
33092
|
const selectedOption = first(result.rooms)?.options?.find((x) => x.isSelected) ?? first(result.rooms)?.options?.[0];
|
|
33065
33093
|
const price = formatPrice$3(result.price, result.currencyCode, languageCode);
|
|
33066
33094
|
const nights = calculateNights(new Date(result.fromDate), new Date(result.toDate));
|
|
33095
|
+
const candidate = findCandidateForResult(node, result);
|
|
33096
|
+
const image = candidate?.imageUrl ?? null;
|
|
33097
|
+
const description = toPlainText(candidate?.description);
|
|
33098
|
+
const title = candidate?.name ?? result.name;
|
|
33099
|
+
const priceBlock = (React__default.createElement("div", { className: "search__result-card__price__wrapper" },
|
|
33100
|
+
React__default.createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
|
|
33101
|
+
React__default.createElement("span", { className: "search__result-card__price" }, price)));
|
|
33067
33102
|
const selectButton = (React__default.createElement("button", { type: "button", className: `cta ${isSelected ? 'cta--selected' : 'cta--select'}`, onClick: onSelect }, isSelected ? translations?.SHARED.SELECTED : translations?.SHARED.SELECT));
|
|
33068
33103
|
if (result.contents?.length) {
|
|
33069
33104
|
return (React__default.createElement("div", { className: `search__result-card__wrapper search__result-card__wrapper--custom ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
@@ -33071,21 +33106,22 @@ const TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSel
|
|
|
33071
33106
|
React__default.createElement("div", { className: "search__result-card__footer" }, selectButton)));
|
|
33072
33107
|
}
|
|
33073
33108
|
return (React__default.createElement("div", { className: `search__result-card__wrapper ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
33109
|
+
image && (React__default.createElement("div", { className: "search__result-card__img-wrapper" },
|
|
33110
|
+
React__default.createElement("img", { src: image, alt: title, className: "search__result-card__img" }),
|
|
33111
|
+
priceBlock)),
|
|
33074
33112
|
React__default.createElement("div", { className: "search__result-card__content" },
|
|
33075
33113
|
React__default.createElement("div", { className: "search__result-card__content__wrapper" },
|
|
33076
33114
|
React__default.createElement("div", { className: "search__result-card__header" },
|
|
33077
33115
|
React__default.createElement("div", { className: "search__result-card__header__wrapper" },
|
|
33078
33116
|
!!result.stars && (React__default.createElement("div", { className: "rating" }, [...Array(result.stars)].map((_, index) => (React__default.createElement(Icon, { name: "ui-star", key: `rating-star-${index + 1}`, width: 14, height: 14 }))))),
|
|
33079
|
-
React__default.createElement("h3", { className: "search__result-card__title" },
|
|
33080
|
-
|
|
33081
|
-
React__default.createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
|
|
33082
|
-
React__default.createElement("span", { className: "search__result-card__price" }, price))),
|
|
33117
|
+
React__default.createElement("h3", { className: "search__result-card__title" }, title)),
|
|
33118
|
+
!image && priceBlock),
|
|
33083
33119
|
React__default.createElement("span", { className: "search__result-card__location" },
|
|
33084
33120
|
React__default.createElement(Icon, { name: "ui-location", height: 16 }),
|
|
33085
33121
|
getLocation(result)),
|
|
33086
33122
|
React__default.createElement("div", { className: "search__result-card__options" },
|
|
33087
33123
|
showNights && nights > 0 && (React__default.createElement("div", { className: "search__result-card__option" },
|
|
33088
|
-
React__default.createElement(Icon, { name: "ui-
|
|
33124
|
+
React__default.createElement(Icon, { name: "ui-moon", height: 16 }),
|
|
33089
33125
|
nights,
|
|
33090
33126
|
" ",
|
|
33091
33127
|
translations?.SRP.NIGHTS)),
|
|
@@ -33094,7 +33130,8 @@ const TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSel
|
|
|
33094
33130
|
selectedOption.accommodationName)),
|
|
33095
33131
|
selectedOption?.regimeName && (React__default.createElement("div", { className: "search__result-card__option" },
|
|
33096
33132
|
React__default.createElement(Icon, { name: "ui-utensils", height: 16 }),
|
|
33097
|
-
selectedOption.regimeName)))
|
|
33133
|
+
selectedOption.regimeName))),
|
|
33134
|
+
description && React__default.createElement("p", { className: "search__result-card__description" }, description)),
|
|
33098
33135
|
React__default.createElement("div", { className: "search__result-card__footer" }, selectButton))));
|
|
33099
33136
|
};
|
|
33100
33137
|
|
|
@@ -33150,7 +33187,8 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33150
33187
|
const isExpanded = !!expandedNodes[line.guid];
|
|
33151
33188
|
const results = availability?.results ?? [];
|
|
33152
33189
|
const selected = results.find((x) => x.code === availability?.selectedCode) ?? null;
|
|
33153
|
-
const
|
|
33190
|
+
const configured = results.filter((x) => x.code !== selected?.code && !!findCandidateForResult(node, x));
|
|
33191
|
+
const others = results.filter((x) => x.code !== selected?.code && !findCandidateForResult(node, x));
|
|
33154
33192
|
// No entry yet means the node's search has been queued but not dispatched — still loading.
|
|
33155
33193
|
const status = availability?.status ?? 'loading';
|
|
33156
33194
|
return (React__default.createElement(React__default.Fragment, { key: line.guid },
|
|
@@ -33159,9 +33197,7 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33159
33197
|
React__default.createElement("p", { className: "search__results__label__date-date" }, format$2(new Date(line.from), 'd', { locale })),
|
|
33160
33198
|
React__default.createElement("p", null, format$2(new Date(line.from), 'MMM', { locale }))),
|
|
33161
33199
|
React__default.createElement("div", { className: "search__results__label__text" },
|
|
33162
|
-
React__default.createElement(Icon, { name: getNodeIcon(node), height: 16 }),
|
|
33163
|
-
React__default.createElement("h3", null,
|
|
33164
|
-
React__default.createElement("strong", null, node.name)))),
|
|
33200
|
+
React__default.createElement(Icon, { name: getNodeIcon(node), height: 16 }))),
|
|
33165
33201
|
status === 'loading' && (React__default.createElement(Spinner, { label: node.serviceType === EXCURSION_SERVICE_TYPE ? translations.SRP.LOADING_EXCURSIONS : translations.SRP.LOADING_ACCOMMODATIONS })),
|
|
33166
33202
|
status === 'unavailable' && React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
|
|
33167
33203
|
status === 'resolved' && selected && (React__default.createElement(React__default.Fragment, null,
|
|
@@ -33170,12 +33206,9 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33170
33206
|
node.preferredAccommodationName,
|
|
33171
33207
|
" \u2014 ",
|
|
33172
33208
|
translations.SRP.NO_RESULTS))),
|
|
33173
|
-
React__default.createElement("div", { className: "search__results__cards search__results__cards--extended" },
|
|
33174
|
-
|
|
33175
|
-
isExpanded
|
|
33176
|
-
alternatives.map((result) => (React__default.createElement(TrajectNodeCard, { key: result.code, result: result, isSelected: false, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, result.code), showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE })))),
|
|
33177
|
-
alternatives.length > 0 && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
33178
|
-
React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => toggleExpanded(line.guid) }, isExpanded ? translations.SRP.SHOW_LESS : `${translations.SRP.SHOW_MORE} (${alternatives.length})`)))))));
|
|
33209
|
+
React__default.createElement("div", { className: "search__results__cards search__results__cards--extended" }, [selected, ...configured, ...(isExpanded ? others : [])].map((result) => (React__default.createElement(TrajectNodeCard, { key: result.code, item: item, result: result, isSelected: result.code === selected.code, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, result.code), showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE })))),
|
|
33210
|
+
others.length > 0 && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
33211
|
+
React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => toggleExpanded(line.guid) }, isExpanded ? translations.SRP.SHOW_LESS : `${translations.SRP.SHOW_MORE} (${others.length})`)))))));
|
|
33179
33212
|
};
|
|
33180
33213
|
const renderFlightNode = (item) => {
|
|
33181
33214
|
const { node, line } = item;
|
|
@@ -33216,36 +33249,23 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33216
33249
|
selectedFlight && (React__default.createElement(IndependentFlightOption, { key: `flight-${selectedKey}`, item: isOutbound ? selectedFlight.outward : selectedFlight.return, guid: selectedFlight.outwardGuid, selectedGuid: selectedFlight.outwardGuid, isOutward: isOutbound, showSelectedState: true, price: selectedFlight.price, onSelect: isOutbound ? () => dispatch(setSelectedOutwardKey(null)) : undefined })),
|
|
33217
33250
|
visible.map((result) => (React__default.createElement(IndependentFlightOption, { key: `flight-${result.outwardGuid}`, item: isOutbound ? result.outward : result.return, guid: result.outwardGuid, isOutward: isOutbound, price: result.price, currentSelectedPrice: selectedFlight?.price, onSelect: () => dispatch(isOutbound ? setSelectedOutwardKey(getFlightKey(result.outward.segments)) : setSelectedReturnKey(getFlightKey(result.return.segments))) })))))));
|
|
33218
33251
|
};
|
|
33219
|
-
/** Connecting markers and points of interest: shown for context, nothing to book. */
|
|
33220
|
-
const renderContextNode = (node) => (React__default.createElement("div", { className: "search__results__cards__actions", key: `${node.nodeId}-context` },
|
|
33221
|
-
React__default.createElement("span", null,
|
|
33222
|
-
React__default.createElement(Icon, { name: "ui-car", height: 14 }),
|
|
33223
|
-
" ",
|
|
33224
|
-
node.name,
|
|
33225
|
-
node.distanceInKm ? ` — ${Math.round(node.distanceInKm)} km` : '')));
|
|
33226
33252
|
if (isLoading)
|
|
33227
33253
|
return React__default.createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
|
|
33228
33254
|
return (React__default.createElement(React__default.Fragment, null, trajectEntry.traject.days.map((day) => {
|
|
33229
33255
|
const dayNodes = nodesByDay.get(day.order) ?? [];
|
|
33230
33256
|
if (!dayNodes.length)
|
|
33231
33257
|
return null;
|
|
33232
|
-
return (React__default.createElement(React__default.Fragment, { key: `day-${day.order}` },
|
|
33233
|
-
|
|
33234
|
-
|
|
33235
|
-
|
|
33236
|
-
|
|
33237
|
-
|
|
33238
|
-
|
|
33239
|
-
|
|
33240
|
-
|
|
33241
|
-
|
|
33242
|
-
|
|
33243
|
-
return renderContextNode(node);
|
|
33244
|
-
const item = lineByNodeId.get(node.nodeId);
|
|
33245
|
-
if (!item)
|
|
33246
|
-
return renderContextNode(node);
|
|
33247
|
-
return isTrajectFlightNode(node) ? renderFlightNode(item) : renderServiceNode(item);
|
|
33248
|
-
})));
|
|
33258
|
+
return (React__default.createElement(React__default.Fragment, { key: `day-${day.order}` }, dayNodes.map((node) => {
|
|
33259
|
+
// if (node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) return null;
|
|
33260
|
+
// if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE) return renderContextNode(node);
|
|
33261
|
+
if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE)
|
|
33262
|
+
return null;
|
|
33263
|
+
const item = lineByNodeId.get(node.nodeId);
|
|
33264
|
+
// if (!item) return renderContextNode(node);
|
|
33265
|
+
if (!item)
|
|
33266
|
+
return null;
|
|
33267
|
+
return isTrajectFlightNode(node) ? renderFlightNode(item) : renderServiceNode(item);
|
|
33268
|
+
})));
|
|
33249
33269
|
})));
|
|
33250
33270
|
};
|
|
33251
33271
|
|
|
@@ -33279,25 +33299,27 @@ const useTrajectAvailability = () => {
|
|
|
33279
33299
|
const search = item.node.serviceType === EXCURSION_SERVICE_TYPE ? build.searchPackagingExcursions : build.searchPackagingAccommodations;
|
|
33280
33300
|
dispatch(setTrajectNodeLoading(lineGuid));
|
|
33281
33301
|
try {
|
|
33282
|
-
const
|
|
33283
|
-
|
|
33284
|
-
|
|
33285
|
-
|
|
33286
|
-
|
|
33287
|
-
const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, code), controller.signal);
|
|
33288
|
-
const hit = (attempt ?? []).find((result) => matchesCode(result, code));
|
|
33289
|
-
if (hit) {
|
|
33290
|
-
results = attempt;
|
|
33291
|
-
picked = hit;
|
|
33292
|
-
break;
|
|
33302
|
+
const candidates = getNodeCandidates(item.node);
|
|
33303
|
+
const attempts = await Promise.all(candidates.map(async (candidate) => {
|
|
33304
|
+
try {
|
|
33305
|
+
const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, candidate.code), controller.signal);
|
|
33306
|
+
return (attempt ?? []).find((result) => matchesCode(result, candidate.code)) ?? null;
|
|
33293
33307
|
}
|
|
33294
|
-
|
|
33295
|
-
|
|
33308
|
+
catch (error) {
|
|
33309
|
+
if (!controller.signal.aborted)
|
|
33310
|
+
console.error('Traject hotel unavailable', candidate.code, error);
|
|
33311
|
+
return null;
|
|
33312
|
+
}
|
|
33313
|
+
}));
|
|
33314
|
+
let results = attempts.filter((x) => !!x).filter((x, i, all) => all.findIndex((y) => y.code === x.code) === i);
|
|
33315
|
+
let picked = results[0] ?? null;
|
|
33316
|
+
const preferredCandidate = candidates.find((x) => x.isPreferred) ?? candidates[0];
|
|
33317
|
+
let preferredUnavailable = !!preferredCandidate && !results.some((x) => matchesCode(x, preferredCandidate.code));
|
|
33296
33318
|
// Nothing configured was available — fall back to the cheapest result in the destination.
|
|
33297
33319
|
if (!picked) {
|
|
33298
33320
|
results = (await search(config, buildTrajectNodeRequest(item, searchContext, ''), controller.signal)) ?? [];
|
|
33299
33321
|
picked = pickCheapest(results);
|
|
33300
|
-
preferredUnavailable =
|
|
33322
|
+
preferredUnavailable = candidates.length > 0;
|
|
33301
33323
|
}
|
|
33302
33324
|
if (controller.signal.aborted)
|
|
33303
33325
|
return null;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { PackagingAccommodationResponse } from '@qite/tide-client';
|
|
3
|
+
import { TrajectNodeWithLine } from '../../utils/traject-utils';
|
|
3
4
|
interface TrajectNodeCardProps {
|
|
5
|
+
item: TrajectNodeWithLine;
|
|
4
6
|
result: PackagingAccommodationResponse;
|
|
5
7
|
isSelected: boolean;
|
|
6
8
|
languageCode?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { PackagingAccommodationRequest, PackagingAccommodationResponse, PackagingEntry, PackagingEntryLine, PackagingFlightResponse, PackagingRoom, PackagingTrajectNode, PackagingTrajectResponse } from '@qite/tide-client';
|
|
1
|
+
import { PackagingAccommodationRequest, PackagingAccommodationResponse, PackagingEntry, PackagingEntryLine, PackagingFlightResponse, PackagingRoom, PackagingTrajectAlternative, PackagingTrajectNode, PackagingTrajectResponse } from '@qite/tide-client';
|
|
2
2
|
export declare const REGULAR_TRAJECT_NODE_TYPE = 0;
|
|
3
|
+
export declare const CONNECTING_TRAJECT_NODE_TYPE = 1;
|
|
3
4
|
export interface TrajectNodeWithLine {
|
|
4
5
|
node: PackagingTrajectNode;
|
|
5
6
|
line: PackagingEntryLine;
|
|
@@ -18,7 +19,8 @@ export interface TrajectSearchContext {
|
|
|
18
19
|
rooms: PackagingAccommodationRequest['rooms'];
|
|
19
20
|
}
|
|
20
21
|
export declare const buildTrajectNodeRequest: ({ node, line }: TrajectNodeWithLine, context: TrajectSearchContext, productCode: string) => PackagingAccommodationRequest;
|
|
21
|
-
export declare const
|
|
22
|
+
export declare const getNodeCandidates: (node: PackagingTrajectNode) => PackagingTrajectAlternative[];
|
|
23
|
+
export declare const findCandidateForResult: (node: PackagingTrajectNode, result: PackagingAccommodationResponse) => PackagingTrajectAlternative | null;
|
|
22
24
|
export declare const matchesCode: (result: PackagingAccommodationResponse, code: string) => boolean;
|
|
23
25
|
export declare const getPackagingRoomsFromEntry: (entry: PackagingEntry) => PackagingRoom[];
|
|
24
26
|
export declare const pickCheapest: (results: PackagingAccommodationResponse[]) => PackagingAccommodationResponse | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qite/tide-components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "React components for Tide (booking, product availability, form, ...)",
|
|
5
5
|
"main": "build/build-cjs/index.js",
|
|
6
6
|
"types": "build/build-cjs/src/index.d.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@jsonurl/jsonurl": "^1.1.4",
|
|
38
38
|
"@popperjs/core": "^2.10.2",
|
|
39
|
-
"@qite/tide-client": "^2.0.
|
|
39
|
+
"@qite/tide-client": "^2.0.12",
|
|
40
40
|
"@reduxjs/toolkit": "^2.8.2",
|
|
41
41
|
"@rollup/plugin-commonjs": "^19.0.1",
|
|
42
42
|
"@rollup/plugin-json": "^4.1.0",
|