@escapenavigator/hooks 1.10.101 → 1.10.103

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/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export type LocalstoreSetValue<T> = T | undefined | ((prev: T | undefined) => T | undefined);
2
- export declare function useLocalstoreState<T>(token: string, defaultValue?: T): [T | undefined, (update: LocalstoreSetValue<T>) => void];
1
+ export * from './use-localstore-state';
2
+ export * from './use-players-and-variation-picker';
3
+ export * from './players-and-variation-picker';
package/dist/index.js CHANGED
@@ -1,43 +1,19 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useLocalstoreState = void 0;
4
- const react_1 = require("react");
5
- function useLocalstoreState(token, defaultValue) {
6
- const [value, setValue] = (0, react_1.useState)(() => {
7
- if (typeof window === 'undefined') {
8
- return defaultValue;
9
- }
10
- try {
11
- const prev = localStorage.getItem(token);
12
- if (prev != null) {
13
- return JSON.parse(prev);
14
- }
15
- }
16
- catch (_a) {
17
- localStorage.removeItem(token);
18
- }
19
- return defaultValue;
20
- });
21
- const setStoredValue = (0, react_1.useCallback)((update) => {
22
- setValue((prev) => {
23
- const next = typeof update === 'function'
24
- ? update(prev)
25
- : update;
26
- if (typeof window === 'undefined') {
27
- return next;
28
- }
29
- if (next === prev) {
30
- return prev;
31
- }
32
- if (next) {
33
- localStorage.setItem(token, JSON.stringify(next));
34
- }
35
- else {
36
- localStorage.removeItem(token);
37
- }
38
- return next;
39
- });
40
- }, [token]);
41
- return [value, setStoredValue];
42
- }
43
- exports.useLocalstoreState = useLocalstoreState;
17
+ __exportStar(require("./use-localstore-state"), exports);
18
+ __exportStar(require("./use-players-and-variation-picker"), exports);
19
+ __exportStar(require("./players-and-variation-picker"), exports);
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import { ProfileCurrencyEnum } from '@escapenavigator/types/dist/profile/enum/profile-currency';
3
+ import { usePlayersAndVariationPicker } from '../use-players-and-variation-picker';
4
+ import { PickerSlotDetails, PickerSlotInfo, PickerSlotVariation, PickerVariationChange } from '../use-players-and-variation-picker/types';
5
+ type LoadSlotDetailsApi = Parameters<typeof usePlayersAndVariationPicker>[0]['loadSlotDetails'];
6
+ export type PlayersAndVariationPickerProps = {
7
+ /**
8
+ * Minimal slot identity used to fetch enriched details. Pass
9
+ * `null`/`undefined` to disable the variation row entirely.
10
+ */
11
+ slot: PickerSlotInfo | null | undefined;
12
+ /** Currently selected tariff id (typically `order.slot.tariffId`). */
13
+ currentTariffId?: number;
14
+ /** Currently selected duration in minutes (flex slots only). */
15
+ currentDuration?: number;
16
+ /** Slot details api method (`slots.slotDetails` or `openapiWidget.slotDetails`). */
17
+ loadSlotDetails: LoadSlotDetailsApi;
18
+ /**
19
+ * Called when the user picks a different variation. Receives the
20
+ * coordinate-based change payload that can be forwarded to the backend
21
+ * without a local cache lookup.
22
+ */
23
+ onVariationChange?: (change: PickerVariationChange) => void;
24
+ /**
25
+ * Notifies the host whenever the embedded picker resolves the currently
26
+ * highlighted variation (initial load or user click). Useful when the
27
+ * host needs the variation's tariff/duration outside of the picker UI
28
+ * (e.g. to render a price list or seed Formik initial values).
29
+ */
30
+ onVariationResolved?: (variation: PickerSlotVariation | undefined) => void;
31
+ /**
32
+ * Notifies the host once `/slots/details` resolves. Exposes the slot
33
+ * coordinates and `isFlex` flag so callers can decide whether to send a
34
+ * persisted `slotId` (fix) or rely on coordinate-based booking (flex).
35
+ */
36
+ onDetailsLoaded?: (details: PickerSlotDetails | null | undefined) => void;
37
+ /** Players form field value. */
38
+ players: number;
39
+ /** Children form field value (defaults to 0). */
40
+ child?: number;
41
+ /**
42
+ * Override for the price map used to render player buttons. Defaults to
43
+ * the selected variation's tariff; pass an explicit value when the host
44
+ * already maintains its own tariff state (e.g. legacy flows).
45
+ */
46
+ tariff?: Record<string, number>;
47
+ currency: ProfileCurrencyEnum;
48
+ /** Minimum players for save validation. */
49
+ minPlayers?: number;
50
+ /**
51
+ * Capacity cap (questroom max, ticket-system `numSeatsAvailable`, etc.).
52
+ * Player/children buttons exceeding `min(maxFromTariff, maxPlayersLimit)`
53
+ * are filtered out (not just disabled).
54
+ */
55
+ maxPlayersLimit?: number;
56
+ /**
57
+ * Slot discount in basis points (1% = 100). Applied to the team price
58
+ * shown in the hint, mirroring the widget behaviour.
59
+ */
60
+ slotDiscount?: number;
61
+ /** External validation error label (controlled by the host form). */
62
+ error?: string;
63
+ /**
64
+ * Translator. Required for the embedded `usePlayersAndVariationPicker`
65
+ * (it composes the default flex duration label).
66
+ */
67
+ t: (key: string, options?: Record<string, unknown>) => string;
68
+ /** Fires on every player/children change. */
69
+ onChange?: (field: 'players' | 'children', value: number) => void;
70
+ /**
71
+ * When provided, renders a Save button inside the picker. The default
72
+ * `players`/`children` state is buffered locally until the user saves.
73
+ */
74
+ onSave?: (data: {
75
+ players: number;
76
+ children: number;
77
+ }) => void;
78
+ /**
79
+ * When editing an existing order, pass the order id here so the backend
80
+ * excludes the current booking from occupancy calculations.
81
+ */
82
+ orderId?: number;
83
+ };
84
+ /**
85
+ * Single shared component that renders the "Variation + Players" picker used
86
+ * by the admin order modals, the widget order flow and the orders package.
87
+ *
88
+ * Rationale: the variation-picker logic lives behind
89
+ * `usePlayersAndVariationPicker`, but the surrounding UI (Tag row, player
90
+ * buttons, kids row, price hint, save button) was reimplemented in three
91
+ * places with subtle drift. This component is the single source of truth.
92
+ */
93
+ export declare const PlayersAndVariationPicker: React.FC<PlayersAndVariationPickerProps>;
94
+ export {};
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.PlayersAndVariationPicker = void 0;
27
+ /* eslint-disable complexity */
28
+ /* eslint-disable no-underscore-dangle */
29
+ /* eslint-disable @typescript-eslint/naming-convention */
30
+ const react_1 = __importStar(require("react"));
31
+ const icons_1 = require("@alphakits/icons");
32
+ const dist_1 = require("@alphakits/ui/dist");
33
+ const format_amount_1 = require("@escapenavigator/utils/dist/format-amount");
34
+ const get_players_price_1 = require("@escapenavigator/utils/dist/get-players-price");
35
+ const get_prices_1 = require("@escapenavigator/utils/dist/get-prices");
36
+ const use_players_and_variation_picker_1 = require("../use-players-and-variation-picker");
37
+ /**
38
+ * Single shared component that renders the "Variation + Players" picker used
39
+ * by the admin order modals, the widget order flow and the orders package.
40
+ *
41
+ * Rationale: the variation-picker logic lives behind
42
+ * `usePlayersAndVariationPicker`, but the surrounding UI (Tag row, player
43
+ * buttons, kids row, price hint, save button) was reimplemented in three
44
+ * places with subtle drift. This component is the single source of truth.
45
+ */
46
+ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loadSlotDetails, onVariationChange, onVariationResolved, onDetailsLoaded, players = 0, child = 0, tariff: tariffOverride, currency, minPlayers, maxPlayersLimit, slotDiscount, error, t, onChange, onSave, orderId, }) => {
47
+ var _a, _b;
48
+ const { loading, options: pickerOptions, selectedOptionId, selectedVariation, buildVariationChange, selectVariation, details, } = (0, use_players_and_variation_picker_1.usePlayersAndVariationPicker)({
49
+ slot,
50
+ currentTariffId,
51
+ currentDuration,
52
+ loadSlotDetails,
53
+ t: t,
54
+ orderId,
55
+ });
56
+ (0, react_1.useEffect)(() => {
57
+ onDetailsLoaded === null || onDetailsLoaded === void 0 ? void 0 : onDetailsLoaded(details);
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ }, [details === null || details === void 0 ? void 0 : details.slotId, details === null || details === void 0 ? void 0 : details.isFlex, (_a = details === null || details === void 0 ? void 0 : details.variations) === null || _a === void 0 ? void 0 : _a.length]);
60
+ const tariff = (0, react_1.useMemo)(() => (tariffOverride || (selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.tariff) || {}), [tariffOverride, selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.tariff]);
61
+ (0, react_1.useEffect)(() => {
62
+ onVariationResolved === null || onVariationResolved === void 0 ? void 0 : onVariationResolved(selectedVariation);
63
+ // eslint-disable-next-line react-hooks/exhaustive-deps
64
+ }, [selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.id, selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.tariffId, selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.duration]);
65
+ const [_players, setPlayers] = (0, react_1.useState)(players);
66
+ const [_children, setChildren] = (0, react_1.useState)(child);
67
+ (0, react_1.useEffect)(() => {
68
+ setPlayers(players);
69
+ }, [players]);
70
+ (0, react_1.useEffect)(() => {
71
+ setChildren(child);
72
+ }, [child]);
73
+ const handleChangePlayers = (value) => {
74
+ setPlayers(value);
75
+ onChange === null || onChange === void 0 ? void 0 : onChange('players', value);
76
+ };
77
+ const handleChangeChildren = (value) => {
78
+ setChildren(value);
79
+ onChange === null || onChange === void 0 ? void 0 : onChange('children', value);
80
+ };
81
+ const playersOptions = (0, react_1.useMemo)(() => Object.entries(tariff)
82
+ .filter(([key]) => key !== 'children' && !Number.isNaN(+key))
83
+ .map(([key, price]) => ({
84
+ key: +key,
85
+ price,
86
+ })), [tariff]);
87
+ const playersArray = (0, get_prices_1.getPricesPlayersArray)(tariff);
88
+ const maxFromTariff = playersArray.length ? +playersArray[playersArray.length - 1] : 0;
89
+ const effectiveMax = typeof maxPlayersLimit === 'number' && Number.isFinite(maxPlayersLimit)
90
+ ? Math.min(maxFromTariff, maxPlayersLimit)
91
+ : maxFromTariff;
92
+ const childrenOptions = (0, react_1.useMemo)(() => {
93
+ if (!tariff.child || !maxFromTariff)
94
+ return [];
95
+ const maxChildren = maxFromTariff + 1;
96
+ return Array.from({ length: maxChildren }, (_, i) => i);
97
+ }, [tariff, maxFromTariff]);
98
+ const teamPriceBase = (0, get_players_price_1.getPlayersPrice)({
99
+ players: _players,
100
+ children: _children,
101
+ price: tariff,
102
+ });
103
+ const teamPrice = slotDiscount
104
+ ? teamPriceBase - Math.floor((teamPriceBase / 10000) * slotDiscount)
105
+ : teamPriceBase;
106
+ const totalHeads = _players + (_children || 0);
107
+ const personPrice = totalHeads > 0 ? Math.floor(teamPrice / totalHeads) : 0;
108
+ const showVariations = pickerOptions.length > 1;
109
+ const variationDescription = (_b = selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.description) !== null && _b !== void 0 ? _b : null;
110
+ const handleVariationClick = (optionId, isFree) => {
111
+ if (!isFree)
112
+ return;
113
+ selectVariation(optionId);
114
+ const change = buildVariationChange(optionId);
115
+ if (!change)
116
+ return;
117
+ onVariationChange === null || onVariationChange === void 0 ? void 0 : onVariationChange(change);
118
+ };
119
+ if (loading && !details) {
120
+ return (react_1.default.createElement(dist_1.FlexColumns, { columns: 1, gr: 16 },
121
+ react_1.default.createElement(dist_1.Skeleton, { visible: true, animate: true },
122
+ react_1.default.createElement("div", { style: { height: 80 } })),
123
+ react_1.default.createElement(dist_1.Skeleton, { visible: true, animate: true },
124
+ react_1.default.createElement("div", { style: { height: 60 } }))));
125
+ }
126
+ return (react_1.default.createElement(dist_1.FlexColumns, { columns: 1, gr: 16 },
127
+ showVariations && (react_1.default.createElement(dist_1.FlexColumns, { columns: 1, gr: 8 },
128
+ react_1.default.createElement(dist_1.Typography.Text, { view: "title", color: "secondary" }, t('prices:variation')),
129
+ react_1.default.createElement(dist_1.Flex, { gap: "xs", wrap: true, justify: "start" }, pickerOptions.map((option) => (react_1.default.createElement("div", { key: option.id, style: option.isFree
130
+ ? undefined
131
+ : { opacity: 0.4, pointerEvents: 'none' } },
132
+ react_1.default.createElement(dist_1.Tag, { view: selectedOptionId === option.id
133
+ ? 'primary-inverted'
134
+ : 'primary', text: option.label, onClick: () => handleVariationClick(option.id, option.isFree) }))))),
135
+ !!variationDescription && (react_1.default.createElement(dist_1.Typography.Text, { view: "caps", color: "secondary" }, variationDescription)))),
136
+ react_1.default.createElement(dist_1.FlexColumns, { columns: 1, gr: 8 },
137
+ react_1.default.createElement(dist_1.Typography.Text, { view: "title", color: "secondary" },
138
+ tariff.child ? t('prices:adults') : t('prices:choosePlayersAmount'),
139
+ ' ',
140
+ !!_players && tariff[_players] != null && (`(${(0, format_amount_1.formatAmount)(tariff[_players], currency)})`)),
141
+ react_1.default.createElement(dist_1.Flex, { gap: "xs", wrap: true, justify: "start" }, playersOptions
142
+ .filter((p) => p.key + (_children || 0) <= effectiveMax)
143
+ .map((p) => (react_1.default.createElement(dist_1.Button, { key: `adult-${p.key}`, size: "xs", style: { width: 40, minWidth: 40 }, onClick: () => handleChangePlayers(p.key), view: _players === p.key ? 'primary' : 'outlined' }, p.key))))),
144
+ !!childrenOptions.length && (react_1.default.createElement(dist_1.FlexColumns, { columns: 1, gr: 8 },
145
+ react_1.default.createElement(dist_1.Typography.Text, { view: "title", color: "secondary" },
146
+ t('prices:kids'),
147
+ ' ',
148
+ !!_children && tariff.child != null && (`(${(0, format_amount_1.formatAmount)(_children * tariff.child, currency)})`)),
149
+ react_1.default.createElement(dist_1.Flex, { gap: "xs", wrap: true, justify: "start" }, childrenOptions
150
+ .filter((count) => _players + count <= effectiveMax)
151
+ .map((count) => {
152
+ const isActive = _children === count;
153
+ return (react_1.default.createElement(dist_1.Button, { key: `child-${count}`, size: "xs", style: { width: 40, minWidth: 40 }, onClick: () => handleChangeChildren(isActive ? 0 : count), view: isActive ? 'primary' : 'outlined' }, count));
154
+ })))),
155
+ !!totalHeads && (react_1.default.createElement(dist_1.Flex, { justify: "start", gap: "xs" },
156
+ react_1.default.createElement(icons_1.InfoMarkS, { style: { fill: 'var(--color-text-secondary)' } }),
157
+ react_1.default.createElement(dist_1.Typography.Text, { view: "caps", color: "secondary" }, t('prices:totalPriceDescription', {
158
+ teamPrice: (0, format_amount_1.formatAmount)(teamPrice, currency),
159
+ personPrice: (0, format_amount_1.formatAmount)(personPrice, currency),
160
+ })))),
161
+ !!error && (react_1.default.createElement(dist_1.ToastPlate, { view: "negative" }, error || t('prices:playersError', { minPlayers, maxPlayers: effectiveMax }))),
162
+ !!onSave && (react_1.default.createElement(dist_1.Button, { onClick: () => onSave({ players: _players, children: _children }), view: "primary", size: "xs" }, t('common:save')))));
163
+ };
164
+ exports.PlayersAndVariationPicker = PlayersAndVariationPicker;
@@ -0,0 +1,2 @@
1
+ export type LocalstoreSetValue<T> = T | undefined | ((prev: T | undefined) => T | undefined);
2
+ export declare function useLocalstoreState<T>(token: string, defaultValue?: T): [T | undefined, (update: LocalstoreSetValue<T>) => void];
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useLocalstoreState = void 0;
4
+ const react_1 = require("react");
5
+ function readFromLocalstore(token, defaultValue) {
6
+ if (typeof window === 'undefined') {
7
+ return defaultValue;
8
+ }
9
+ try {
10
+ const prev = localStorage.getItem(token);
11
+ if (prev != null) {
12
+ return JSON.parse(prev);
13
+ }
14
+ }
15
+ catch (_a) {
16
+ localStorage.removeItem(token);
17
+ }
18
+ return defaultValue;
19
+ }
20
+ function useLocalstoreState(token, defaultValue) {
21
+ // We track defaultValue via ref so token changes don't pull in stale closures
22
+ // while also avoiding extra effect runs when consumers pass an inline default.
23
+ const defaultValueRef = (0, react_1.useRef)(defaultValue);
24
+ defaultValueRef.current = defaultValue;
25
+ const [value, setValue] = (0, react_1.useState)(() => readFromLocalstore(token, defaultValue));
26
+ const prevTokenRef = (0, react_1.useRef)(token);
27
+ (0, react_1.useEffect)(() => {
28
+ if (prevTokenRef.current === token)
29
+ return;
30
+ prevTokenRef.current = token;
31
+ setValue(readFromLocalstore(token, defaultValueRef.current));
32
+ }, [token]);
33
+ const setStoredValue = (0, react_1.useCallback)((update) => {
34
+ setValue((prev) => {
35
+ const next = typeof update === 'function'
36
+ ? update(prev)
37
+ : update;
38
+ if (typeof window === 'undefined') {
39
+ return next;
40
+ }
41
+ if (next === prev) {
42
+ return prev;
43
+ }
44
+ if (next) {
45
+ localStorage.setItem(token, JSON.stringify(next));
46
+ }
47
+ else {
48
+ localStorage.removeItem(token);
49
+ }
50
+ return next;
51
+ });
52
+ }, [token]);
53
+ return [value, setStoredValue];
54
+ }
55
+ exports.useLocalstoreState = useLocalstoreState;
@@ -0,0 +1,92 @@
1
+ import { PickerOption, PickerSlotDetails, PickerSlotInfo, PickerSlotVariation, PickerVariationChange } from './types';
2
+ export * from './types';
3
+ type LoadSlotDetailsApi = {
4
+ (params: {
5
+ slotId?: number;
6
+ questroomId: number;
7
+ date: string;
8
+ start: string;
9
+ orderId?: number;
10
+ }, transportConfig?: any): Promise<{
11
+ data: PickerSlotDetails;
12
+ }>;
13
+ displayName: string;
14
+ };
15
+ type Params = {
16
+ /**
17
+ * Top-level slot record fetched from the listing. Used to identify the
18
+ * slot for the `/details` request and to seed the booking payload.
19
+ */
20
+ slot: PickerSlotInfo | null | undefined;
21
+ /**
22
+ * Currently selected tariff id (typically `order.slot.tariffId`). Used to
23
+ * highlight the active variation. For flex slots also pass
24
+ * `currentDuration` so the picker can disambiguate between options that
25
+ * share a tariff.
26
+ */
27
+ currentTariffId?: number;
28
+ /**
29
+ * Currently selected game duration in minutes. Optional, only relevant
30
+ * for flex slots; ignored for fix slots.
31
+ */
32
+ currentDuration?: number;
33
+ /**
34
+ * Service-layer API method that returns enriched slot details.
35
+ * Pass `slots.slotDetails` (admin) or `openapiWidget.slotDetails` (widget).
36
+ */
37
+ loadSlotDetails: LoadSlotDetailsApi;
38
+ /**
39
+ * Translator. Only used for the default flex duration label suffix.
40
+ */
41
+ t: (key: string) => string;
42
+ /**
43
+ * When editing an existing order, pass the order id here so the backend
44
+ * excludes the current booking from occupancy calculations.
45
+ * This prevents the edited order from blocking its own slot/duration.
46
+ */
47
+ orderId?: number;
48
+ };
49
+ type Result = {
50
+ loading: boolean;
51
+ isFlex: boolean;
52
+ options: PickerOption[];
53
+ selectedVariation: PickerSlotVariation | undefined;
54
+ /**
55
+ * Stable id of the highlighted picker option. Use this (and not
56
+ * `tariffId`) when comparing against `option.id` in the UI — flex
57
+ * variations may share a tariff across durations.
58
+ */
59
+ selectedOptionId: string | undefined;
60
+ /**
61
+ * Variation hint text. For fix slots returns the selected non-default
62
+ * variation title; for flex returns null (caller may compose its own
63
+ * duration label from `selectedVariation`).
64
+ */
65
+ hint: string | null;
66
+ /**
67
+ * Translate a clicked picker option into a coordinate-based booking
68
+ * payload that can be forwarded to the create-order / update-slot
69
+ * endpoints without any local cache lookup.
70
+ */
71
+ buildVariationChange: (optionId: string) => PickerVariationChange | null;
72
+ /**
73
+ * Force-update the internal selected option (used by the picker UI on
74
+ * click, before the host has had a chance to push back updated
75
+ * `currentTariffId`/`currentDuration` props).
76
+ */
77
+ selectVariation: (optionId: string) => void;
78
+ details: PickerSlotDetails | null | undefined;
79
+ };
80
+ /**
81
+ * Single source of truth for the variation picker shared between admin order
82
+ * modals, widget order flow and the orders package.
83
+ *
84
+ * Internally fetches `/slots/details` via the supplied api method and exposes
85
+ * a normalised contract over the unified `variations[]` payload:
86
+ * - `options` and `selectedOptionId` for rendering tag/button rows
87
+ * - `selectedVariation` for callers that need the underlying variation
88
+ * (e.g. to compute prices)
89
+ * - `buildVariationChange(id)` for translating a tap to a coordinate-based
90
+ * booking payload `{ questroomId, date, start, tariffId, duration, slotId? }`
91
+ */
92
+ export declare const usePlayersAndVariationPicker: ({ slot, currentTariffId, currentDuration, loadSlotDetails, t, orderId, }: Params) => Result;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.usePlayersAndVariationPicker = void 0;
18
+ /* eslint-disable no-nested-ternary */
19
+ const react_1 = require("react");
20
+ const use_api_method_1 = require("@escapenavigator/services/dist/hooks/use-api-method");
21
+ __exportStar(require("./types"), exports);
22
+ const variationKey = (variation) => { var _a; return (_a = variation.id) !== null && _a !== void 0 ? _a : `${variation.tariffId}-${variation.duration}`; };
23
+ /**
24
+ * Single source of truth for the variation picker shared between admin order
25
+ * modals, widget order flow and the orders package.
26
+ *
27
+ * Internally fetches `/slots/details` via the supplied api method and exposes
28
+ * a normalised contract over the unified `variations[]` payload:
29
+ * - `options` and `selectedOptionId` for rendering tag/button rows
30
+ * - `selectedVariation` for callers that need the underlying variation
31
+ * (e.g. to compute prices)
32
+ * - `buildVariationChange(id)` for translating a tap to a coordinate-based
33
+ * booking payload `{ questroomId, date, start, tariffId, duration, slotId? }`
34
+ */
35
+ const usePlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loadSlotDetails, t, orderId, }) => {
36
+ const api = (0, use_api_method_1.useApiMethod)({ api: loadSlotDetails });
37
+ const data = api[`${loadSlotDetails.displayName}Data`];
38
+ const loading = !!api[`${loadSlotDetails.displayName}Loading`];
39
+ const fetch = api[`${loadSlotDetails.displayName}Fetch`];
40
+ (0, react_1.useEffect)(() => {
41
+ if (!slot)
42
+ return;
43
+ fetch({
44
+ slotId: slot.id,
45
+ questroomId: slot.questroomId,
46
+ date: slot.date,
47
+ start: slot.start,
48
+ orderId,
49
+ });
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, [slot === null || slot === void 0 ? void 0 : slot.id, slot === null || slot === void 0 ? void 0 : slot.questroomId, slot === null || slot === void 0 ? void 0 : slot.date, slot === null || slot === void 0 ? void 0 : slot.start, orderId]);
52
+ const variations = (0, react_1.useMemo)(() => (data === null || data === void 0 ? void 0 : data.variations) || [], [data === null || data === void 0 ? void 0 : data.variations]);
53
+ const isFlex = !!(data === null || data === void 0 ? void 0 : data.isFlex);
54
+ const findCurrentVariation = (rows) => {
55
+ if (!rows.length)
56
+ return undefined;
57
+ if (isFlex) {
58
+ // Match by (tariffId, duration) when both are known; fall back to
59
+ // duration-only or tariff-only when one is missing.
60
+ if (currentTariffId != null && currentDuration != null) {
61
+ const exact = rows.find((row) => row.tariffId === currentTariffId
62
+ && row.duration === currentDuration);
63
+ if (exact)
64
+ return exact;
65
+ }
66
+ if (currentDuration != null) {
67
+ const byDuration = rows.find((row) => row.duration === currentDuration);
68
+ if (byDuration)
69
+ return byDuration;
70
+ }
71
+ if (currentTariffId != null) {
72
+ const byTariff = rows.find((row) => row.tariffId === currentTariffId);
73
+ if (byTariff)
74
+ return byTariff;
75
+ }
76
+ return rows.find((row) => row.isDefault) || rows[0];
77
+ }
78
+ if (currentTariffId != null) {
79
+ const byTariff = rows.find((row) => row.tariffId === currentTariffId);
80
+ if (byTariff)
81
+ return byTariff;
82
+ }
83
+ return rows.find((row) => row.isDefault) || rows[0];
84
+ };
85
+ const propsSelectedVariation = (0, react_1.useMemo)(() => findCurrentVariation(variations),
86
+ // eslint-disable-next-line react-hooks/exhaustive-deps
87
+ [variations, isFlex, currentTariffId, currentDuration]);
88
+ const propsSelectedOptionId = propsSelectedVariation
89
+ ? variationKey(propsSelectedVariation)
90
+ : undefined;
91
+ // Internal click state — lets the picker switch its highlighted option
92
+ // immediately, even if the host doesn't push back updated
93
+ // `currentTariffId`/`currentDuration` props. We re-seed it whenever the
94
+ // prop-derived option changes (initial load, host explicitly changing
95
+ // the order's tariff, etc.).
96
+ const [clickedOptionId, setClickedOptionId] = (0, react_1.useState)(undefined);
97
+ (0, react_1.useEffect)(() => {
98
+ setClickedOptionId(undefined);
99
+ }, [propsSelectedOptionId, data === null || data === void 0 ? void 0 : data.slotId]);
100
+ const selectedOptionId = clickedOptionId !== null && clickedOptionId !== void 0 ? clickedOptionId : propsSelectedOptionId;
101
+ const selectedVariation = (0, react_1.useMemo)(() => {
102
+ var _a;
103
+ return (_a = variations.find((variation) => variationKey(variation) === selectedOptionId)) !== null && _a !== void 0 ? _a : propsSelectedVariation;
104
+ }, [variations, selectedOptionId, propsSelectedVariation]);
105
+ const options = (0, react_1.useMemo)(() => {
106
+ if (variations.length < 2)
107
+ return [];
108
+ return variations.map((variation) => {
109
+ var _a;
110
+ const id = variationKey(variation);
111
+ const isCurrent = id === selectedOptionId;
112
+ const label = isFlex
113
+ ? `${variation.duration} ${t('common:минут')}`
114
+ : (variation.isDefault
115
+ ? 'Default'
116
+ : variation.title || `Tariff ${variation.tariffId}`);
117
+ return {
118
+ id,
119
+ tariffId: variation.tariffId,
120
+ duration: variation.duration,
121
+ label,
122
+ description: !isFlex ? (_a = variation.description) !== null && _a !== void 0 ? _a : null : null,
123
+ isCurrent,
124
+ isFree: !!variation.isFree,
125
+ };
126
+ });
127
+ }, [variations, isFlex, selectedOptionId, t]);
128
+ const hint = (0, react_1.useMemo)(() => {
129
+ if (!isFlex && selectedVariation && !selectedVariation.isDefault) {
130
+ return selectedVariation.title || null;
131
+ }
132
+ return null;
133
+ }, [isFlex, selectedVariation]);
134
+ const buildVariationChange = (optionId) => {
135
+ if (!data)
136
+ return null;
137
+ const variation = variations.find((row) => variationKey(row) === optionId);
138
+ if (!variation)
139
+ return null;
140
+ return {
141
+ questroomId: data.questroomId,
142
+ date: data.date,
143
+ start: data.start,
144
+ slotId: data.slotId,
145
+ tariffId: variation.tariffId,
146
+ duration: variation.duration,
147
+ };
148
+ };
149
+ /**
150
+ * Update internal click state. Call from the UI before notifying the
151
+ * host so the highlighted Tag updates without round-tripping through
152
+ * controlled props.
153
+ */
154
+ const selectVariation = (optionId) => {
155
+ setClickedOptionId(optionId);
156
+ };
157
+ return {
158
+ loading,
159
+ isFlex,
160
+ options,
161
+ selectedVariation,
162
+ selectedOptionId,
163
+ hint,
164
+ buildVariationChange,
165
+ selectVariation,
166
+ details: data,
167
+ };
168
+ };
169
+ exports.usePlayersAndVariationPicker = usePlayersAndVariationPicker;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Shared shapes used by `usePlayersAndVariationPicker`.
3
+ *
4
+ * They intentionally mirror both admin (`SlotRO`/`SlotEnrichedRO`) and widget
5
+ * (`OpenapiSlotRO`/`OpenapiSlotEnrichedRO`) contracts so a single picker can
6
+ * be plugged into either context.
7
+ */
8
+ export type PickerSlotVariation = {
9
+ /**
10
+ * Stable picker key returned by the backend (`${tariffId}-${duration}`).
11
+ * Optional only for backwards compatibility while older hosts roll out
12
+ * the new field; once everyone is on the new contract this becomes the
13
+ * authoritative picker option id.
14
+ */
15
+ id?: string;
16
+ tariffId: number;
17
+ duration: number;
18
+ title: string;
19
+ description?: string;
20
+ isDefault: boolean;
21
+ isFree: boolean;
22
+ tariff: {
23
+ [k: string]: number;
24
+ };
25
+ minPlayers: number;
26
+ maxPlayers: number;
27
+ numSeatsAvailable: number;
28
+ };
29
+ export type PickerSlotDetails = {
30
+ questroomId: number;
31
+ date: string;
32
+ start: string;
33
+ /** Persisted parent slot id; absent for flex slots. */
34
+ slotId?: number;
35
+ isFlex: boolean;
36
+ variations: PickerSlotVariation[];
37
+ numSeatsAvailable: number;
38
+ minPlayers: number;
39
+ };
40
+ /**
41
+ * Minimal slot identity needed to fetch enriched details. Everything else
42
+ * (tariff, ruleId, discount, ...) lives in the host's order/slot record;
43
+ * the picker reads its own copy from `/slots/details`.
44
+ */
45
+ export type PickerSlotInfo = {
46
+ id: number;
47
+ questroomId: number;
48
+ date: string;
49
+ start: string;
50
+ };
51
+ export type PickerOption = {
52
+ /**
53
+ * Stable picker option key. Mirrors `PickerSlotVariation.id`
54
+ * (`${tariffId}-${duration}`) so that flex variations sharing a tariff
55
+ * across durations stay distinguishable.
56
+ */
57
+ id: string;
58
+ tariffId: number;
59
+ duration: number;
60
+ label: string;
61
+ description?: string | null;
62
+ isCurrent: boolean;
63
+ /**
64
+ * True when the variation is currently bookable. Unbookable variations
65
+ * are still rendered (greyed out) so the user sees the full duration
66
+ * list returned by `/slots/details`.
67
+ */
68
+ isFree: boolean;
69
+ };
70
+ /**
71
+ * Coordinate-based booking payload produced by the picker on user change.
72
+ *
73
+ * Frontend forwards these fields to the backend without any local cache
74
+ * lookup. Backend resolves the actual slot via `(slotId)` for fix slots or
75
+ * via `(questroomId, date, start, duration, tariffId)` for flex slots.
76
+ */
77
+ export type PickerVariationChange = {
78
+ questroomId: number;
79
+ date: string;
80
+ start: string;
81
+ tariffId: number;
82
+ duration: number;
83
+ /** Persisted parent slot id; only set for fix slots. */
84
+ slotId?: number;
85
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * Shared shapes used by `usePlayersAndVariationPicker`.
4
+ *
5
+ * They intentionally mirror both admin (`SlotRO`/`SlotEnrichedRO`) and widget
6
+ * (`OpenapiSlotRO`/`OpenapiSlotEnrichedRO`) contracts so a single picker can
7
+ * be plugged into either context.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/hooks",
3
- "version": "1.10.101",
3
+ "version": "1.10.103",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -14,9 +14,16 @@
14
14
  "test": "jest"
15
15
  },
16
16
  "dependencies": {
17
- "@escapenavigator/types": "^1.10.101",
17
+ "@escapenavigator/services": "^1.10.111",
18
+ "@escapenavigator/types": "^1.10.102",
19
+ "@escapenavigator/utils": "^1.10.106",
18
20
  "react": "^18.3.1"
19
21
  },
22
+ "peerDependencies": {
23
+ "@alphakits/icons": "*",
24
+ "@alphakits/ui": "*",
25
+ "react-i18next": "*"
26
+ },
20
27
  "devDependencies": {
21
28
  "@rollup/plugin-node-resolve": "^13.0.0",
22
29
  "@rollup/plugin-replace": "^2.4.2",
@@ -41,5 +48,5 @@
41
48
  "react": "^18.3.1",
42
49
  "axios": "^0.21.1"
43
50
  },
44
- "gitHead": "ed7be71b51074fef0f397a620a7b06edee3f5d54"
51
+ "gitHead": "2b24c3c15eb1add8eba28d8f90ca97f5fac884ab"
45
52
  }