bookito-widget 0.42.16-beta.2 → 0.42.16-beta.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bookito-widget",
3
- "version": "0.42.16-beta.2",
3
+ "version": "0.42.16-beta.21",
4
4
  "main": "dist/bookito-widget.umd.js",
5
5
  "license": "MIT",
6
6
  "module": "build/esm/index.js",
@@ -1,9 +1,8 @@
1
- import { getImageFormatUrl, useBookitoSettings } from '@carlito/api';
2
- import { LocalizedEventData } from '@carlito/bookito-client';
1
+ import { getImageFormatUrl, useApp, useBookitoSettings } from '@carlito/api';
2
+ import { LocalizedEventData, isEventBookable } from '@carlito/bookito-client';
3
3
  import { Trans } from 'react-i18next';
4
4
  import { BookingForm } from '../BookitoForm/NewBookingForm';
5
- import { isEventBookable } from '../BookitoForm/helper';
6
- import { BookitoWidgetViewMode } from '../types';
5
+ import { BookitoWidgetViewMode, ResolvedOccurrence } from '../types';
7
6
  import { useMemo } from 'react';
8
7
 
9
8
  export type Props = {
@@ -13,6 +12,8 @@ export type Props = {
13
12
  availableOnDates: string[];
14
13
  isModalView?: boolean;
15
14
  viewMode?: BookitoWidgetViewMode;
15
+ /** Server-resolved occurrence for `date` (resolveOccurrences=true). */
16
+ resolvedOccurrence?: ResolvedOccurrence;
16
17
  };
17
18
 
18
19
  function BookitoEventDetail({
@@ -22,8 +23,10 @@ function BookitoEventDetail({
22
23
  date,
23
24
  isModalView = false,
24
25
  viewMode,
26
+ resolvedOccurrence,
25
27
  }: Props) {
26
28
  const { description, bannerImg, title } = event;
29
+ const { timezone } = useApp();
27
30
 
28
31
  const { bookitoSettings } = useBookitoSettings({ hotelId: authId });
29
32
  const { isBookable } = useMemo(
@@ -32,8 +35,9 @@ function BookitoEventDetail({
32
35
  event,
33
36
  onDate: date,
34
37
  settings: bookitoSettings,
38
+ timezone,
35
39
  }),
36
- [bookitoSettings, date, event]
40
+ [bookitoSettings, date, event, timezone]
37
41
  );
38
42
 
39
43
  let isBookableEvent = isBookable;
@@ -81,6 +85,8 @@ function BookitoEventDetail({
81
85
  authId={authId}
82
86
  availableOnDates={availableOnDates}
83
87
  shouldPreselectDateInCalendar={shouldPreselectDateInCalendar ?? !isModalView}
88
+ resolvedOccurrence={resolvedOccurrence}
89
+ timezone={timezone}
84
90
  />
85
91
  ) : (
86
92
  <p className="bookito-event-detail__description text-sm lg:text-base leading-relaxed ">
@@ -1,7 +1,5 @@
1
- import { useBookitoSettings } from '@carlito/api';
1
+ import { useBookitoSettings, useHotelDate } from '@carlito/api';
2
2
  import { SupportedLanguages, useListLocalizedEvents } from '@carlito/bookito-client';
3
- import { useLayoutEffect } from 'react';
4
- import { useTranslation } from 'react-i18next';
5
3
  import BookitoEventDetail from '../BookitoEventDetail';
6
4
  import Modal from '../Modal';
7
5
  import ModalLoader from './ModalLoader';
@@ -14,31 +12,21 @@ export type BookitoEventModalProps = {
14
12
  openModal: boolean;
15
13
  };
16
14
 
17
- const BOOKABLE_DAYS_IN_ADVANCE = 90;
18
- const defaultToDate = new Date();
19
-
20
- defaultToDate.setDate(defaultToDate.getDate() + BOOKABLE_DAYS_IN_ADVANCE);
21
-
22
15
  function BookitoEventModal(props: BookitoEventModalProps) {
23
- const { i18n } = useTranslation();
24
- useLayoutEffect(() => {
25
- if (props?.language) {
26
- i18n.changeLanguage(props?.language);
27
- }
28
- }, [props?.language, i18n]);
29
-
30
16
  const bookitoSettings = useBookitoSettings({
31
17
  hotelId: props.authId,
32
18
  });
33
19
 
20
+ const daysBookableInAdvance = bookitoSettings.bookitoSettings?.daysBookableInAdvance ?? 90;
21
+ const toDate = useHotelDate({ days: daysBookableInAdvance });
22
+
34
23
  const { data, isLoading } = useListLocalizedEvents(
35
24
  {
36
25
  authId: props.authId,
37
26
  id: props.eventId,
38
- toDate: defaultToDate.toISOString(),
39
- daysBookableInAdvance: (
40
- bookitoSettings.bookitoSettings?.daysBookableInAdvance ?? 90
41
- ).toString(),
27
+ toDate: toDate.toISOString(),
28
+ daysBookableInAdvance: daysBookableInAdvance.toString(),
29
+ resolveOccurrences: true,
42
30
  },
43
31
  props.openModal && Boolean(bookitoSettings.bookitoSettings)
44
32
  );
@@ -49,6 +37,20 @@ function BookitoEventModal(props: BookitoEventModalProps) {
49
37
  ? new Date(data?.availableOnDates?.[0])
50
38
  : new Date();
51
39
 
40
+ // First server-resolved occurrence for this event (resolveOccurrences=true), used as the
41
+ // preselected occurrence in the booking form so no client RRULE runs.
42
+ const resolvedOccurrence = (() => {
43
+ if (!data?.dates) return undefined;
44
+ const sortedByDay = Object.entries(data.dates).sort(
45
+ ([a], [b]) => new Date(a).getTime() - new Date(b).getTime()
46
+ );
47
+ for (const [, day] of sortedByDay) {
48
+ const occurrence = day.events.find((e) => e.id === props.eventId)?.occurrences?.[0];
49
+ if (occurrence) return occurrence;
50
+ }
51
+ return undefined;
52
+ })();
53
+
52
54
  return props.openModal ? (
53
55
  <Modal onCloseModal={props.onCloseModal}>
54
56
  {isLoading && <ModalLoader />}
@@ -59,6 +61,7 @@ function BookitoEventModal(props: BookitoEventModalProps) {
59
61
  authId={props.authId}
60
62
  availableOnDates={data.availableOnDates || []}
61
63
  isModalView={true}
64
+ resolvedOccurrence={resolvedOccurrence}
62
65
  />
63
66
  )}
64
67
  </Modal>
@@ -1,8 +1,6 @@
1
1
  import React from 'react';
2
- import { findOccurrenceForDate, LocalizedEventData } from '@carlito/bookito-client';
2
+ import { LocalizedEventData } from '@carlito/bookito-client';
3
3
  import humanizeDuration, { SupportedLanguage } from 'humanize-duration';
4
- import { applyTimeOfDate } from '@carlito/date-utils';
5
- import { isEventAllDay } from './helper';
6
4
  import { Trans } from 'react-i18next';
7
5
  import { formatPrice, useApp } from '@carlito/api';
8
6
 
@@ -8,18 +8,13 @@ import {
8
8
  } from '@carlito/shared-ui';
9
9
  import { analyticsEvent } from '@carlito/app-analytics';
10
10
  import { SUPPORTED_LANGUAGE_CODES } from '@carlito/intl';
11
- import {
12
- extractApiErrorCodes,
13
- findOccurrenceForDate,
14
- LocalizedEventData,
15
- } from '@carlito/bookito-client';
11
+ import { extractApiErrorCodes, LocalizedEventData } from '@carlito/bookito-client';
16
12
  import { useGetHotelConfiguration } from '@carlito/hub-react';
17
13
  import { Formik } from 'formik';
18
14
  import EventInfo from './EventInfo';
19
15
  import FormSubmitButton from './FormSubmitButton';
20
- import { applyTimeOfDate } from '@carlito/date-utils';
21
- import { isEventAllDay } from './helper';
22
16
  import { useTranslation } from 'react-i18next';
17
+ import { ResolvedOccurrence } from '../types';
23
18
 
24
19
  function getUserLanguage() {
25
20
  const browserLanguages = [...navigator.languages];
@@ -40,6 +35,9 @@ export type BookingFormProps = {
40
35
  authId: string;
41
36
  availableOnDates: string[];
42
37
  shouldPreselectDateInCalendar?: boolean;
38
+ /** Server-resolved occurrence for the preselected date (resolveOccurrences=true). */
39
+ resolvedOccurrence?: ResolvedOccurrence;
40
+ timezone?: string;
43
41
  };
44
42
 
45
43
  export function BookingForm({
@@ -48,17 +46,21 @@ export function BookingForm({
48
46
  authId,
49
47
  availableOnDates,
50
48
  shouldPreselectDateInCalendar = true,
49
+ resolvedOccurrence,
50
+ timezone,
51
51
  }: BookingFormProps) {
52
52
  const userLanguage = getUserLanguage();
53
53
 
54
54
  const { t } = useTranslation();
55
- const occurrence = findOccurrenceForDate(onDate, event) ?? event?.occurrences[0];
56
- const startDateTime = applyTimeOfDate(onDate, new Date(occurrence.startDateTime));
57
- const isAllDay = event ? isEventAllDay(startDateTime, event) : undefined;
55
+ // Occurrence times come from the server-resolved occurrence (resolveOccurrences=true) —
56
+ // no client RRULE. When absent, the calendar simply isn't preselected.
57
+ const startDateTime = resolvedOccurrence ? new Date(resolvedOccurrence.startDateTime) : undefined;
58
+ const durationMs = resolvedOccurrence?.durationMs;
59
+ const isAllDay = resolvedOccurrence?.isAllDay;
58
60
  const overallDuration =
59
61
  (event?.timeBookability === 'custom'
60
62
  ? event?.timeBookabilitySettings?.timeBlockDurationMs
61
- : occurrence?.durationMs) ?? occurrence.durationMs;
63
+ : durationMs) ?? undefined;
62
64
 
63
65
  const initialValues: Partial<BookitoBookingFormValues> = useMemo(() => {
64
66
  return {
@@ -123,6 +125,7 @@ export function BookingForm({
123
125
  activeLanguage={userLanguage}
124
126
  event={event}
125
127
  hotelId={authId}
128
+ timezone={timezone}
126
129
  isTimeSlotEnabled={isTimeSlotEnabled}
127
130
  availableOnDates={availableOnDates}
128
131
  renderQuantityControls={({
@@ -17,8 +17,8 @@ import ExperienceCardHolder from '../CardHolder';
17
17
  import Modal from '../Modal';
18
18
  import CarlitoLogo from '../assets/carlito-logo.svg';
19
19
  import styles from '../index.module.css';
20
- import { BookitoWidgetViewMode } from '../types';
21
- import { useApp } from '@carlito/api';
20
+ import { BookitoWidgetViewMode, ResolvedOccurrence } from '../types';
21
+ import { useApp, useHotelDate } from '@carlito/api';
22
22
  const { getCssModuleClassName } = cssModuleClassNames(styles);
23
23
 
24
24
  // Helper function to extract actual data from InfiniteData or return the data directly
@@ -44,15 +44,17 @@ export type BookitoWidgetProps = {
44
44
  excludeNonBookable?: boolean;
45
45
  };
46
46
 
47
- type SelectedEvent = { event: LocalizedEventData; date: Date };
48
-
49
- const dateToday = new Date();
50
- dateToday.setHours(0, 0, 0, 0);
47
+ type SelectedEvent = {
48
+ event: LocalizedEventData;
49
+ date: Date;
50
+ occurrence?: ResolvedOccurrence;
51
+ };
51
52
 
52
53
  const BookitoWidget = (props: BookitoWidgetProps) => {
53
54
  const { viewMode = 'Date' } = props;
54
55
  const { i18n } = useTranslation();
55
56
  const { timezone } = useApp();
57
+ const dateToday = useHotelDate();
56
58
  const [canFetchCategoryList, setFetchingCategoryList] = useState(false);
57
59
  const [categoryTitle, setCategoryTitle] = useState('');
58
60
 
@@ -87,6 +89,7 @@ const BookitoWidget = (props: BookitoWidgetProps) => {
87
89
  authId: props?.authId,
88
90
  categoryId: props?.categoryId,
89
91
  excludeNonBookable: props.excludeNonBookable,
92
+ resolveOccurrences: true,
90
93
  });
91
94
 
92
95
  const listEventCategoriesQuery = useListLocalizedEventCategories(
@@ -153,11 +156,30 @@ const BookitoWidget = (props: BookitoWidgetProps) => {
153
156
  return null;
154
157
  }
155
158
 
159
+ // Earliest server-resolved occurrence per event across the loaded range
160
+ // (resolveOccurrences=true), so the flat list renders times without client RRULE.
161
+ const occurrencesByEventId: Record<string, ResolvedOccurrence> = {};
162
+ if (eventsData.dates) {
163
+ const sortedDays = Object.entries(eventsData.dates).sort(
164
+ ([a], [b]) => new Date(a).getTime() - new Date(b).getTime()
165
+ );
166
+ for (const [, day] of sortedDays) {
167
+ for (const entry of day.events) {
168
+ const occurrence = entry.occurrences?.[0];
169
+ if (occurrence && !occurrencesByEventId[entry.id]) {
170
+ occurrencesByEventId[entry.id] = occurrence;
171
+ }
172
+ }
173
+ }
174
+ }
175
+
156
176
  return (
157
177
  <ExperienceCardHolder
158
- onDate={dateToday}
159
178
  events={eventsData.events}
160
- openModal={(event) => setSelectedEvent({ event, date: dateToday })}
179
+ occurrences={occurrencesByEventId}
180
+ openModal={(event) =>
181
+ setSelectedEvent({ event, date: dateToday, occurrence: occurrencesByEventId[event.id] })
182
+ }
161
183
  />
162
184
  );
163
185
  };
@@ -192,6 +214,16 @@ const BookitoWidget = (props: BookitoWidgetProps) => {
192
214
  return null;
193
215
  }
194
216
 
217
+ // Server-resolved occurrence per event for this day (resolveOccurrences=true),
218
+ // so Cards render times without client RRULE.
219
+ const occurrencesByEventId: Record<string, ResolvedOccurrence> = {};
220
+ for (const entry of events) {
221
+ const occurrence = entry.occurrences?.[0];
222
+ if (occurrence) {
223
+ occurrencesByEventId[entry.id] = occurrence;
224
+ }
225
+ }
226
+
195
227
  // Interpret dateStr as local date in hotel's timezone
196
228
  const date = DateTime.fromISO(dateStr, { zone: timezone }).startOf('day').toJSDate();
197
229
 
@@ -203,13 +235,16 @@ const BookitoWidget = (props: BookitoWidgetProps) => {
203
235
  weekday: 'long',
204
236
  month: 'long',
205
237
  day: 'numeric',
238
+ timeZone: timezone,
206
239
  })
207
240
  )}
208
241
  </span>
209
242
  <ExperienceCardHolder
210
- onDate={date}
211
243
  events={eventsToRender}
212
- openModal={(event) => setSelectedEvent({ event, date })}
244
+ occurrences={occurrencesByEventId}
245
+ openModal={(event) =>
246
+ setSelectedEvent({ event, date, occurrence: occurrencesByEventId[event.id] })
247
+ }
213
248
  />
214
249
  </div>
215
250
  );
@@ -254,6 +289,7 @@ const BookitoWidget = (props: BookitoWidgetProps) => {
254
289
  authId={props?.authId}
255
290
  availableOnDates={getAvailabilityDates(selectedEvent.event.id)}
256
291
  viewMode={viewMode}
292
+ resolvedOccurrence={selectedEvent.occurrence}
257
293
  />
258
294
  </Modal>
259
295
  )}
@@ -11,7 +11,7 @@ const BookitoWidgetProvider: React.FC<{
11
11
  }> = (props) => {
12
12
  return (
13
13
  <ApolloProvider client={client}>
14
- <AppProvider hotelId={props.hotelId}>
14
+ <AppProvider defaultLanguage={props.language} hotelId={props.hotelId}>
15
15
  <ContentLanguageProvider
16
16
  skipHotelLanguages={true}
17
17
  browserLocale={props.language ?? (i18n.language as SupportedLocalesCode)}
@@ -1,37 +1,45 @@
1
1
  import { LocalizedEventData } from '@carlito/bookito-client';
2
2
  import { LineClamper } from '@carlito/dashboard-ui';
3
- import { applyTimeOfDate } from '@carlito/date-utils';
4
3
  import { useApp } from '@carlito/api';
5
4
  import classNames from 'classnames';
6
- import { isEventAllDay } from '../BookitoForm/helper';
7
- import formatPrice, { findOccurrenceForDate } from '../utils';
5
+ import formatPrice from '../utils';
6
+ import { ResolvedOccurrence } from '../types';
8
7
 
9
- type CardProps = { event: LocalizedEventData; openModal: () => void; onDate: Date };
8
+ type CardProps = {
9
+ event: LocalizedEventData;
10
+ openModal: () => void;
11
+ occurrence?: ResolvedOccurrence;
12
+ };
10
13
 
11
- const ExperienceCard = ({ event, openModal, onDate }: CardProps) => {
14
+ const ExperienceCard = ({ event, openModal, occurrence }: CardProps) => {
12
15
  const { title, basePrice, description, bannerImg } = event;
13
- const { hotelCurrency } = useApp();
14
-
15
- const occurrence = findOccurrenceForDate(onDate, event) ?? event.occurrences[0];
16
+ const { hotelCurrency, timezone } = useApp();
16
17
 
17
- const startDateTime = applyTimeOfDate(onDate, new Date(occurrence.startDateTime));
18
- const endDateTime = new Date(startDateTime);
19
- endDateTime.setTime(endDateTime.getTime() + occurrence.durationMs);
18
+ // Times come straight from the server-resolved occurrence (resolveOccurrences=true):
19
+ // startDateTime is an absolute instant and isAllDay is precomputed — no client RRULE.
20
+ // When no occurrence is supplied the time block is simply hidden.
21
+ const startDateTime = occurrence ? new Date(occurrence.startDateTime) : undefined;
22
+ const endDateTime =
23
+ startDateTime && occurrence
24
+ ? new Date(startDateTime.getTime() + occurrence.durationMs)
25
+ : undefined;
20
26
 
21
- const startDateTimeStr = startDateTime.toLocaleString(undefined, {
27
+ const startDateTimeStr = startDateTime?.toLocaleString(undefined, {
22
28
  hour: '2-digit',
23
29
  minute: '2-digit',
30
+ timeZone: timezone,
24
31
  });
25
32
 
26
- const endDateTimeStr = endDateTime.toLocaleString(undefined, {
33
+ const endDateTimeStr = endDateTime?.toLocaleString(undefined, {
27
34
  hour: '2-digit',
28
35
  minute: '2-digit',
36
+ timeZone: timezone,
29
37
  });
30
38
 
31
- const isAllDay = isEventAllDay(onDate, event);
39
+ const isAllDay = occurrence?.isAllDay ?? false;
32
40
 
33
41
  const shouldShowPrice = Boolean(basePrice?.amount);
34
- const shouldShowTimes = !isAllDay;
42
+ const shouldShowTimes = !isAllDay && Boolean(startDateTimeStr && endDateTimeStr);
35
43
 
36
44
  return (
37
45
  <div
@@ -1,13 +1,15 @@
1
1
  import { LocalizedEventData } from '@carlito/bookito-client';
2
2
  import ExperienceCard from '../Card';
3
+ import { ResolvedOccurrence } from '../types';
3
4
 
4
5
  type Props = {
5
6
  events: LocalizedEventData[];
6
7
  openModal(event: LocalizedEventData): void;
7
- onDate: Date;
8
+ /** Server-resolved occurrence per event id (resolveOccurrences=true). */
9
+ occurrences?: Record<string, ResolvedOccurrence>;
8
10
  };
9
11
 
10
- const ExperienceCardHolder = ({ events, openModal, onDate }: Props) => {
12
+ const ExperienceCardHolder = ({ events, openModal, occurrences }: Props) => {
11
13
  return (
12
14
  <div
13
15
  className="bookito-widget__events-list flex max-h-[200px]"
@@ -20,7 +22,7 @@ const ExperienceCardHolder = ({ events, openModal, onDate }: Props) => {
20
22
  key={event.id}
21
23
  event={event}
22
24
  openModal={() => openModal(event)}
23
- onDate={onDate}
25
+ occurrence={occurrences?.[event.id]}
24
26
  />
25
27
  ))}
26
28
  </div>
@@ -4,3 +4,19 @@
4
4
  border: none;
5
5
  background: none;
6
6
  }
7
+ .calendar-tile {
8
+ flex: 0 0 2.8rem !important;
9
+ height: 2.8rem;
10
+ display: flex;
11
+ justify-content: center;
12
+ align-items: center;
13
+ }
14
+ .calendar-tile.active {
15
+ border: 1px solid var(--theme-color-primary) !important;
16
+ }
17
+
18
+ .react-calendar__month-view__weekdays,
19
+ .react-calendar__month-view__days {
20
+ justify-content: space-around;
21
+ row-gap: 5px;
22
+ }
package/src/types.ts CHANGED
@@ -1 +1,8 @@
1
+ import { ConditionallyLocalizedListEventsResponse } from '@carlito/bookito-client';
2
+
1
3
  export type BookitoWidgetViewMode = 'List' | 'Date';
4
+
5
+ /** Server-resolved occurrence (resolveOccurrences=true): `dates[].events[].occurrences[]`. */
6
+ export type ResolvedOccurrence = NonNullable<
7
+ ConditionallyLocalizedListEventsResponse<true>['dates'][string]['events'][number]['occurrences']
8
+ >[number];
@@ -1,6 +1,3 @@
1
- import { LocalizedEventData, getRRuleConfiguration } from '@carlito/bookito-client';
2
- import { RRule } from 'rrule';
3
-
4
1
  export default function formatPrice(cost: number, locale = 'de-DE', currency: string = 'EUR') {
5
2
  return new Intl.NumberFormat(locale, {
6
3
  style: 'currency',
@@ -8,47 +5,3 @@ export default function formatPrice(cost: number, locale = 'de-DE', currency: st
8
5
  currencyDisplay: 'narrowSymbol',
9
6
  }).format(cost);
10
7
  }
11
-
12
- export function findOccurrenceForDate(
13
- targetDate: Date,
14
- event: Pick<LocalizedEventData, 'occurrences'>
15
- ) {
16
- for (const occurrence of event.occurrences) {
17
- const startDateTime = new Date(occurrence.startDateTime);
18
-
19
- if (
20
- startDateTime.getDate() === targetDate.getDate() &&
21
- startDateTime.getMonth() === targetDate.getMonth() &&
22
- startDateTime.getFullYear() === targetDate.getFullYear()
23
- ) {
24
- return occurrence;
25
- }
26
-
27
- if (occurrence.repetition) {
28
- const rruleConfig = getRRuleConfiguration(
29
- {
30
- ...occurrence.repetition,
31
- repeatUntil: occurrence.repetition.repeatUntil
32
- ? new Date(occurrence.repetition.repeatUntil)
33
- : undefined,
34
- },
35
- {
36
- dtStart: startDateTime,
37
- }
38
- );
39
-
40
- const rule = new RRule(rruleConfig);
41
-
42
- const dates = rule.between(
43
- new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()),
44
- new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate(), 23, 59, 59),
45
- true
46
- );
47
-
48
- if (dates.length > 0) {
49
- return occurrence;
50
- }
51
- }
52
- }
53
- return null;
54
- }