@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.
@@ -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 getPreferredCodes = (node) => {
32990
- const codes = [node.code, node.preferredAccommodationCode, ...(node.alternativeAccommodationCodes ?? [])];
32991
- return codes.filter((code) => !!code && code.trim().length > 0).filter((code, index, all) => all.indexOf(code) === index);
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 TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSelect, showNights }) => {
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" }, result.name)),
33109
- React__default["default"].createElement("div", { className: "search__result-card__price__wrapper" },
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-bed", height: 16 }),
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 alternatives = results.filter((x) => x.code !== availability?.selectedCode);
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
- React__default["default"].createElement(TrajectNodeCard, { result: selected, isSelected: true, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, selected.code), showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE }),
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
- React__default["default"].createElement("div", { className: "search__results__label" },
33263
- React__default["default"].createElement("div", { className: "search__results__label__text" },
33264
- React__default["default"].createElement("h2", null,
33265
- translations.ITINERARY.DAY,
33266
- " ",
33267
- day.order + 1,
33268
- day.name ? ` — ${day.name}` : ''),
33269
- React__default["default"].createElement("span", null, dateFns.format(new Date(day.date), 'EEEE d MMMM yyyy', { locale })))),
33270
- dayNodes.map((node) => {
33271
- if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE)
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 preferredCodes = getPreferredCodes(item.node);
33312
- let results = [];
33313
- let picked = null;
33314
- // Try the traject's own product/hotel codes first, in configured order.
33315
- for (const code of preferredCodes) {
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
- let preferredUnavailable = false;
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 = preferredCodes.length > 0;
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 getPreferredCodes: (node: PackagingTrajectNode) => string[];
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;
@@ -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 getPreferredCodes = (node) => {
32961
- const codes = [node.code, node.preferredAccommodationCode, ...(node.alternativeAccommodationCodes ?? [])];
32962
- return codes.filter((code) => !!code && code.trim().length > 0).filter((code, index, all) => all.indexOf(code) === index);
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 TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSelect, showNights }) => {
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" }, result.name)),
33080
- React__default.createElement("div", { className: "search__result-card__price__wrapper" },
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-bed", height: 16 }),
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 alternatives = results.filter((x) => x.code !== availability?.selectedCode);
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
- React__default.createElement(TrajectNodeCard, { result: selected, isSelected: true, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, selected.code), showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE }),
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
- React__default.createElement("div", { className: "search__results__label" },
33234
- React__default.createElement("div", { className: "search__results__label__text" },
33235
- React__default.createElement("h2", null,
33236
- translations.ITINERARY.DAY,
33237
- " ",
33238
- day.order + 1,
33239
- day.name ? ` — ${day.name}` : ''),
33240
- React__default.createElement("span", null, format$2(new Date(day.date), 'EEEE d MMMM yyyy', { locale })))),
33241
- dayNodes.map((node) => {
33242
- if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE)
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 preferredCodes = getPreferredCodes(item.node);
33283
- let results = [];
33284
- let picked = null;
33285
- // Try the traject's own product/hotel codes first, in configured order.
33286
- for (const code of preferredCodes) {
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
- let preferredUnavailable = false;
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 = preferredCodes.length > 0;
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 getPreferredCodes: (node: PackagingTrajectNode) => string[];
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.2",
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.11",
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",